diff --git a/plugins/plugin-maven/che-plugin-maven-server/pom.xml b/plugins/plugin-maven/che-plugin-maven-server/pom.xml index 0ed404ec7c..55ac5f4e07 100644 --- a/plugins/plugin-maven/che-plugin-maven-server/pom.xml +++ b/plugins/plugin-maven/che-plugin-maven-server/pom.xml @@ -74,6 +74,10 @@ org.eclipse.che.core che-core-api-project + + org.eclipse.che.core + che-core-api-project-shared + org.eclipse.che.core che-core-api-workspace-shared diff --git a/plugins/plugin-maven/che-plugin-maven-server/src/main/java/org/eclipse/che/plugin/maven/server/core/project/PomChangeListener.java b/plugins/plugin-maven/che-plugin-maven-server/src/main/java/org/eclipse/che/plugin/maven/server/core/project/PomChangeListener.java index c06bad4dbc..f55eba2b66 100644 --- a/plugins/plugin-maven/che-plugin-maven-server/src/main/java/org/eclipse/che/plugin/maven/server/core/project/PomChangeListener.java +++ b/plugins/plugin-maven/che-plugin-maven-server/src/main/java/org/eclipse/che/plugin/maven/server/core/project/PomChangeListener.java @@ -17,10 +17,11 @@ import com.google.inject.name.Named; import org.eclipse.che.api.core.notification.EventService; import org.eclipse.che.api.core.notification.EventSubscriber; import org.eclipse.che.api.project.server.notification.ProjectItemModifiedEvent; +import org.eclipse.che.api.project.shared.dto.event.PomModifiedEventDto; import org.eclipse.che.commons.schedule.executor.ThreadPullLauncher; +import org.eclipse.che.ide.maven.tools.Model; import org.eclipse.che.plugin.maven.server.core.EclipseWorkspaceProvider; import org.eclipse.che.plugin.maven.server.core.MavenWorkspace; -import org.eclipse.che.ide.maven.tools.Model; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.runtime.Path; @@ -71,6 +72,16 @@ public class PomChangeListener { } } }); + + eventService.subscribe(new EventSubscriber() { + @Override + public void onEvent(PomModifiedEventDto event) { + String eventPath = event.getPath(); + if (pomIsValid(eventPath)) { + projectToUpdate.add(new Path(eventPath).removeLastSegments(1).toOSString()); + } + } + }); } private boolean pomIsValid(String path) { diff --git a/wsagent/che-core-api-project-shared/src/main/java/org/eclipse/che/api/project/shared/dto/event/GitBranchCheckoutEventDto.java b/wsagent/che-core-api-project-shared/src/main/java/org/eclipse/che/api/project/shared/dto/event/GitBranchCheckoutEventDto.java new file mode 100644 index 0000000000..428cc9add5 --- /dev/null +++ b/wsagent/che-core-api-project-shared/src/main/java/org/eclipse/che/api/project/shared/dto/event/GitBranchCheckoutEventDto.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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.project.shared.dto.event; + +import com.google.common.annotations.Beta; + +import org.eclipse.che.dto.shared.DTO; + +/** + * To transfer branch name after git checkout operation + * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@Beta +@DTO +public interface GitBranchCheckoutEventDto { + String getBranchName(); + + GitBranchCheckoutEventDto withBranchName(String branchName); +} diff --git a/wsagent/che-core-api-project-shared/src/main/java/org/eclipse/che/api/project/shared/dto/event/PomModifiedEventDto.java b/wsagent/che-core-api-project-shared/src/main/java/org/eclipse/che/api/project/shared/dto/event/PomModifiedEventDto.java new file mode 100644 index 0000000000..074ee128c9 --- /dev/null +++ b/wsagent/che-core-api-project-shared/src/main/java/org/eclipse/che/api/project/shared/dto/event/PomModifiedEventDto.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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.project.shared.dto.event; + +import com.google.common.annotations.Beta; + +import org.eclipse.che.dto.shared.DTO; + +/** + * To transfer modified POM path + * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@Beta +@DTO +public interface PomModifiedEventDto { + String getPath(); + + PomModifiedEventDto withPath(String path); + +} diff --git a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/project/server/ProjectApiModule.java b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/project/server/ProjectApiModule.java index 5c06b5a439..5665de31a6 100644 --- a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/project/server/ProjectApiModule.java +++ b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/project/server/ProjectApiModule.java @@ -11,6 +11,7 @@ package org.eclipse.che.api.project.server; import com.google.inject.AbstractModule; +import com.google.inject.TypeLiteral; import com.google.inject.multibindings.Multibinder; import com.google.inject.name.Names; @@ -26,6 +27,12 @@ import org.eclipse.che.api.vfs.VirtualFileSystemProvider; import org.eclipse.che.api.vfs.impl.file.DefaultFileWatcherNotificationHandler; import org.eclipse.che.api.vfs.impl.file.FileWatcherNotificationHandler; import org.eclipse.che.api.vfs.impl.file.LocalVirtualFileSystemProvider; +import org.eclipse.che.api.vfs.impl.file.event.GitCheckoutHiEventDetector; +import org.eclipse.che.api.vfs.impl.file.event.HiEventDetector; +import org.eclipse.che.api.vfs.impl.file.event.HiEventService; +import org.eclipse.che.api.vfs.impl.file.event.LoEventListener; +import org.eclipse.che.api.vfs.impl.file.event.LoEventService; +import org.eclipse.che.api.vfs.impl.file.event.PomModifiedHiEventDetector; import org.eclipse.che.api.vfs.search.MediaTypeFilter; import org.eclipse.che.api.vfs.search.SearcherProvider; import org.eclipse.che.api.vfs.search.impl.FSLuceneSearcherProvider; @@ -72,5 +79,15 @@ public class ProjectApiModule extends AbstractModule { bind(VirtualFileSystemProvider.class).to(LocalVirtualFileSystemProvider.class); bind(FileWatcherNotificationHandler.class).to(DefaultFileWatcherNotificationHandler.class); + + bind(LoEventListener.class); + bind(LoEventService.class); + bind(HiEventService.class); + + Multibinder> highLevelVfsEventDetectorMultibinder = + Multibinder.newSetBinder(binder(), new TypeLiteral>() { + }); + highLevelVfsEventDetectorMultibinder.addBinding().to(PomModifiedHiEventDetector.class); + highLevelVfsEventDetectorMultibinder.addBinding().to(GitCheckoutHiEventDetector.class); } } diff --git a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/project/server/ProjectManager.java b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/project/server/ProjectManager.java index 27554c6b46..31f42df003 100644 --- a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/project/server/ProjectManager.java +++ b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/project/server/ProjectManager.java @@ -34,18 +34,16 @@ import org.eclipse.che.api.project.server.type.ProjectTypeRegistry; import org.eclipse.che.api.project.server.type.ProjectTypeResolution; import org.eclipse.che.api.project.server.type.ValueStorageException; import org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType; -import org.eclipse.che.api.project.shared.dto.event.VfsWatchEvent; import org.eclipse.che.api.vfs.Path; import org.eclipse.che.api.vfs.VirtualFile; -import org.eclipse.che.api.vfs.VirtualFileFilter; import org.eclipse.che.api.vfs.VirtualFileSystem; import org.eclipse.che.api.vfs.VirtualFileSystemProvider; import org.eclipse.che.api.vfs.impl.file.FileTreeWatcher; import org.eclipse.che.api.vfs.impl.file.FileWatcherNotificationHandler; import org.eclipse.che.api.vfs.impl.file.FileWatcherNotificationListener; +import org.eclipse.che.api.vfs.impl.file.event.LoEvent; import org.eclipse.che.api.vfs.search.Searcher; import org.eclipse.che.api.vfs.search.SearcherProvider; -import org.eclipse.che.dto.server.DtoFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -109,16 +107,22 @@ public final class ProjectManager { @PostConstruct void initWatcher() throws IOException { - FileWatcherNotificationListener defaultListener = new FileWatcherNotificationListener(VirtualFileFilter.ACCEPT_ALL) { - @Override - public void onFileWatcherEvent(VirtualFile virtualFile, FileWatcherEventType eventType) { - LOG.debug("FS event detected: " + eventType + " " + virtualFile.getPath().toString() + " " + virtualFile.isFile()); - eventService.publish(DtoFactory.newDto(VfsWatchEvent.class) - .withPath(virtualFile.getPath().toString()) - .withFile(virtualFile.isFile()) - .withType(eventType)); - } - }; + FileWatcherNotificationListener defaultListener = + new FileWatcherNotificationListener(file -> !(file.getPath().toString().contains(".codenvy") + || file.getPath().toString().contains(".#"))) { + @Override + public void onFileWatcherEvent(VirtualFile virtualFile, FileWatcherEventType eventType) { + LOG.debug("FS event detected: " + eventType + " " + virtualFile.getPath().toString() + " " + virtualFile.isFile()); + eventService.publish(LoEvent.newInstance() + .withPath(virtualFile.getPath().toString()) + .withName(virtualFile.getName()) + .withItemType(virtualFile.isFile() + ? LoEvent.ItemType.FILE + : LoEvent.ItemType.DIR) + .withTime(System.currentTimeMillis()) + .withEventType(eventType)); + } + }; fileWatchNotifier.addNotificationListener(defaultListener); try { fileWatcher.startup(); diff --git a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/EventTreeHelper.java b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/EventTreeHelper.java new file mode 100644 index 0000000000..be60670cc7 --- /dev/null +++ b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/EventTreeHelper.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import com.google.common.annotations.Beta; + +import org.eclipse.che.api.vfs.Path; + +import java.util.Optional; + +import static java.io.File.separator; +import static org.eclipse.che.api.vfs.impl.file.event.EventTreeNode.newInstance; + +/** + * Helper for event tree related operations. + * + * @see {@link EventTreeNode} + * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@Beta +public class EventTreeHelper { + + /** + * Adds corresponding event to an event tree. + *

+ * If event's tree parents are not yet in the tree they are implicitly created + * with default values and no events. For tree nodes created in such way call + * of {@link EventTreeNode#modificationOccurred()} method always return false. + *

+ * @param root root node of the tree, node where event's absolute path starts + * + * @param loEvent event to be added + */ + public static void addEventAndCreatePrecedingNodes(EventTreeNode root, LoEvent loEvent) { + traverseAndCreate(root, Path.of(loEvent.getPath())) + .withEvent(loEvent) + .withPath(loEvent.getPath()) + .withType(loEvent.getItemType()); + } + + /** + * Get a tree node according to a relative path starting from a predefined tree node. + * + * @param root node where relative path starts + * @param relativePath relative path + * + * @return node if node with such path exists otherwise {@code null} + */ + public static Optional getTreeNode(EventTreeNode root, String relativePath) { + Optional current = Optional.of(root); + + if (relativePath.startsWith(separator)) { + relativePath = relativePath.substring(1); + } + + if (relativePath.endsWith(separator)) { + relativePath = relativePath.substring(0, relativePath.length() - 1); + } + + for (String segment : relativePath.split(separator)) { + if (current.isPresent()) { + current = current.get().getChild(segment); + } + } + + return current; + } + + private static EventTreeNode traverseAndCreate(EventTreeNode root, Path path) { + EventTreeNode current = root; + + for (int i = 0; i < path.length(); i++) { + final String name = path.element(i); + final EventTreeNode parent = current; + final Optional childOptional = current.getChild(name); + + current = childOptional.orElseGet(() -> newInstance().withName(name).withParent(parent)); + } + + return current; + } +} diff --git a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/EventTreeNode.java b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/EventTreeNode.java new file mode 100644 index 0000000000..1fa1b5645d --- /dev/null +++ b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/EventTreeNode.java @@ -0,0 +1,186 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import com.google.common.annotations.Beta; + +import org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType; +import org.eclipse.che.api.vfs.impl.file.event.LoEvent.ItemType; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import static java.util.Optional.empty; +import static java.util.stream.Stream.concat; +import static org.eclipse.che.api.vfs.impl.file.event.LoEvent.ItemType.DIR; +import static org.eclipse.che.api.vfs.impl.file.event.LoEvent.ItemType.FILE; +import static org.eclipse.che.api.vfs.impl.file.event.LoEvent.ItemType.UNDEFINED; + +/** + * Virtual file system event tree is a data structure to store low level VFS + * events in a specific manner. Events are stored in a such way that an event + * tree corresponds to a file system items tree but includes only those tree + * branches that contain modified file system items. + *

+ * By design event tree represents a time-based event segment. All events + * close enough (in terms of time line) to each other are included to the + * tree. + *

+ *

+ * This class uses {@link LinkedHashMap} to store an event chain, not to + * lose data when we have several sequential modifications of a single + * tree item. + *

+ *

+ * Note: for convenience there is a predefined event tree root node - '/', + * which is defined by {@link EventTreeNode#ROOT_NODE_NAME} constant. + * All trees must be started from that node using corresponding factory + * method {@link EventTreeNode#newRootInstance()} + *

+ * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@Beta +public class EventTreeNode { + + /** + * Tree root node name. All properly constructed trees must be started from this node. + */ + private static final String ROOT_NODE_NAME = "/"; + + private List children; + private String name; + private String path; + private ItemType type; + /** + * Event chain to store all events occurred within a single time + * segment with this event tree node instance. + * Key - timestamp in millis, value - event type + */ + private Map events; + + private EventTreeNode() { + this.events = new LinkedHashMap<>(); + this.children = new LinkedList<>(); + this.type = UNDEFINED; + } + + public static EventTreeNode newRootInstance() { + return EventTreeNode.newInstance().withName(ROOT_NODE_NAME); + } + + public static EventTreeNode newInstance() { + return new EventTreeNode(); + } + + public EventTreeNode withName(String name) { + this.name = name; + return this; + } + + public EventTreeNode withParent(EventTreeNode parent) { + parent.withChild(this); + return this; + } + + + public EventTreeNode withChild(EventTreeNode child) { + this.children.add(child); + return this; + } + + public EventTreeNode withEvent(LoEvent loEvent) { + this.events.put(loEvent.getTime(), loEvent.getEventType()); + return this; + } + + public EventTreeNode withPath(String path) { + this.path = path; + return this; + } + + public EventTreeNode withType(ItemType type) { + this.type = type; + return this; + } + + public String getName() { + return name; + } + + public String getPath() { + return path; + } + + public ItemType getType() { + return type; + } + + public List getChildren() { + return children; + } + + public Optional getChild(String name) { + for (EventTreeNode node : children) { + if (node.getName().equals(name)) { + return Optional.of(node); + } + } + + return empty(); + } + + public Optional getFirstChild() { + if (children.isEmpty()) { + return empty(); + } + + return Optional.of(children.get(0)); + } + + public Map getEvents() { + return events; + } + + public FileWatcherEventType getLastEventType() { + final List> entryList = new ArrayList<>(events.entrySet()); + final Map.Entry lastEntry = entryList.get(entryList.size() - 1); + + return lastEntry.getValue(); + } + + public boolean modificationOccurred() { + return !events.isEmpty(); + } + + public boolean isFile() { + return type.equals(FILE); + } + + public boolean isDir() { + return type.equals(DIR); + } + + public boolean isRoot() { + return ROOT_NODE_NAME.equals(name); + } + + public Stream stream() { + return concat(Stream.of(this), this.children.stream().flatMap(EventTreeNode::stream)); + } +} diff --git a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/EventTreeQueueHolder.java b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/EventTreeQueueHolder.java new file mode 100644 index 0000000000..d9dc628550 --- /dev/null +++ b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/EventTreeQueueHolder.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import com.google.common.annotations.Beta; + +import org.slf4j.Logger; + +import javax.inject.Singleton; +import java.util.Optional; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import static java.util.Optional.empty; +import static org.slf4j.LoggerFactory.getLogger; + +/** + * Simple holder to benefit from Guice DI routines. + * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@Beta +@Singleton +class EventTreeQueueHolder { + private static final Logger LOG = getLogger(EventTreeQueueHolder.class); + + private final BlockingQueue loVfsEventQueue; + + public EventTreeQueueHolder() { + this.loVfsEventQueue = new LinkedBlockingQueue<>(); + } + + public void put(EventTreeNode loVfsEventTreeRoot) { + try { + loVfsEventQueue.put(loVfsEventTreeRoot); + } catch (InterruptedException e) { + LOG.error("Error trying to put an event tree to an event tree queue: {}", loVfsEventTreeRoot, e); + } + } + + public Optional take() { + try { + return Optional.of(loVfsEventQueue.take()); + } catch (InterruptedException e) { + LOG.error("Error trying to take an event tree out of an event tree queue", e); + } + return empty(); + } +} diff --git a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/GitCheckoutHiEventDetector.java b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/GitCheckoutHiEventDetector.java new file mode 100644 index 0000000000..a6c58ce5a0 --- /dev/null +++ b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/GitCheckoutHiEventDetector.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import com.google.common.annotations.Beta; + +import org.eclipse.che.api.core.ForbiddenException; +import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.project.shared.dto.event.GitBranchCheckoutEventDto; +import org.eclipse.che.api.vfs.Path; +import org.eclipse.che.api.vfs.VirtualFileSystemProvider; +import org.slf4j.Logger; + +import javax.inject.Inject; +import java.util.Optional; +import java.util.regex.Pattern; + +import static java.io.File.separator; +import static java.util.Optional.empty; +import static java.util.regex.Pattern.compile; +import static org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType.MODIFIED; +import static org.eclipse.che.api.vfs.impl.file.event.HiEvent.Category.UNDEFINED; +import static org.eclipse.che.dto.server.DtoFactory.newDto; +import static org.slf4j.LoggerFactory.getLogger; + +/** + * Detects if there was a .git/HEAD file modification, which is a sign of git branch + * checkout operation, though in some rare cases it simply shows that the head of + * current branch is changed. + *

+ * By the moment of this class creation those situations are considered rare + * enough to ignore false detections. + *

+ *

+ * It is designed to detect only HEAD file MODIFICATION, which means that it will + * not trigger if those files are CREATED, DELETED, etc. + *

+ *

+ * This very implementation works only with git repositories initialized in + * the project root folder. + *

+ * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@Beta +public class GitCheckoutHiEventDetector implements HiEventDetector { + private static final Logger LOG = getLogger(GitCheckoutHiEventDetector.class); + + private static final String GIT_DIR = ".git"; + private static final String HEAD_FILE = "HEAD"; + private static final String GIT_OPERATION_WS_CHANNEL = "git-operations-channel"; + private static final int PRIORITY = 50; + private static final Pattern PATTERN = compile("ref: refs" + separator + "heads" + separator); + + private final VirtualFileSystemProvider virtualFileSystemProvider; + private final HiEventBroadcaster broadcaster; + + @Inject + public GitCheckoutHiEventDetector(VirtualFileSystemProvider virtualFileSystemProvider, + HiEventClientBroadcaster highLevelVfsEventClientBroadcaster) { + this.virtualFileSystemProvider = virtualFileSystemProvider; + this.broadcaster = highLevelVfsEventClientBroadcaster; + } + + @Override + public Optional> detect(EventTreeNode eventTreeNode) { + if (!eventTreeNode.isRoot() || eventTreeNode.getChildren().isEmpty()) { + return empty(); + } + + final Optional headFile = eventTreeNode.getFirstChild() + .flatMap(o -> o.getChild(GIT_DIR)) + .flatMap(o -> o.getChild(HEAD_FILE)); + + if (headFile.isPresent() + && headFile.get().modificationOccurred() + && MODIFIED.equals(headFile.get().getLastEventType())) { + + final GitBranchCheckoutEventDto dto = newDto(GitBranchCheckoutEventDto.class).withBranchName(getBranchName(headFile.get())); + + return Optional.of(HiEvent.newInstance(GitBranchCheckoutEventDto.class) + .withCategory(UNDEFINED.withPriority(PRIORITY)) + .withBroadcaster(broadcaster) + .withChannel(GIT_OPERATION_WS_CHANNEL) + .withDto(dto)); + } else { + return empty(); + } + } + + private String getBranchName(EventTreeNode file) { + try { + final String result = virtualFileSystemProvider.getVirtualFileSystem() + .getRoot() + .getChild(Path.of(file.getPath())) + .getContentAsString(); + return PATTERN.split(result)[1]; + } catch (ServerException | ForbiddenException e) { + LOG.error("Error trying to read {} file and broadcast it", file.getPath(), e); + } + + return ""; + } +} diff --git a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEvent.java b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEvent.java new file mode 100644 index 0000000000..1e03ad42b2 --- /dev/null +++ b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEvent.java @@ -0,0 +1,145 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import com.google.common.annotations.Beta; + +import java.util.LinkedList; +import java.util.List; + +/** + * High level virtual file system event implementation. Unlike {@link LoEvent} + * it is to represent not physical but logical events. In terms of file system + * logical events are those that may be caused by one or more physical events. + * In other words sets of low level VFS events result in high level VFS events. + *

+ * Small example: creation of project's root folder ({@link LoEvent}) + * along with populating it with content (creating new files and folders - + * also {@link LoEvent}) indicates that most likely a project is imported. + * And project import is a {@link HiEvent}. + *

+ *

+ * Note: We consider that the user of this class is responsible for setting + * proper broadcasters and (if it is required) all additional information + * (e.g. web socket channel). + *

+ *

+ * Each high level VFS event instance can have arbitrary number (but not less + * than one) of {@link HiEventBroadcaster} defined. By design an instance + * of {@link HiEventDetector} which is responsible for creating of event + * is also responsible for definition of a list of corresponding broadcasters. + *

+ * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@Beta +public class HiEvent { + /** + * Should be a plain DTO object representing this event. Is designed to be + * further used both for client-side and server-side notifications. + *

+ * Note: The implementation of DTO should fulfill all restrictions and + * be located in {@code org.eclipse.che.api.project.shared.dto.event} + * package + *

+ */ + private T dto; + /** + * Field holds category property of an event. Please see {@link Category} + */ + private Category category; + private List broadcasters; + /** + * Web socket channel name. + *

+ * It is ignored if event is not expected to be sent over web socket + * (e.g. server side event) + *

+ */ + private String channel; + + private HiEvent() { + this.broadcasters = new LinkedList<>(); + } + + public static HiEvent newInstance(Class type) { + return new HiEvent<>(); + } + + public T getDto() { + return dto; + } + + public HiEvent withDto(T dto) { + this.dto = dto; + return this; + } + + public String getChannel() { + return channel; + } + + public HiEvent withChannel(String channel) { + this.channel = channel; + return this; + } + + public Category getCategory() { + return category; + } + + public HiEvent withCategory(Category category) { + this.category = category; + return this; + } + + public HiEvent withBroadcaster(HiEventBroadcaster hiEventBroadcaster) { + this.broadcasters.add(hiEventBroadcaster); + return this; + } + + public void broadcast() { + broadcasters.stream().forEach(o -> o.broadcast(this)); + } + + + /** + * Simple enumeration to represent event categorizing mechanics. + *

+ * The idea is to allow only one high level event per category for + * a single low level events set (tree snapshot). If we have several + * high level events with the same category we are to choose the most + * appropriate event according to its priority. + *

+ *

+ * Note: UNDEFINED is used for all events that have no category. + *

+ */ + @Beta + public enum Category { + UNDEFINED, + PROJECT_INFRASTRUCTURE; + + private long priority; + + public long getPriority() { + return priority; + } + + public Category withPriority(long priority) { + this.priority = priority; + return this; + } + + } +} diff --git a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventBroadcaster.java b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventBroadcaster.java new file mode 100644 index 0000000000..c8ab2aeb6d --- /dev/null +++ b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventBroadcaster.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import com.google.common.annotations.Beta; + +/** + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@Beta +@FunctionalInterface +public interface HiEventBroadcaster { + void broadcast(HiEvent event); +} diff --git a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventBroadcasterManager.java b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventBroadcasterManager.java new file mode 100644 index 0000000000..1a9d013531 --- /dev/null +++ b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventBroadcasterManager.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import com.google.common.annotations.Beta; + +import org.eclipse.che.api.vfs.impl.file.event.HiEvent.Category; + +import javax.inject.Singleton; +import java.util.List; + +import static java.lang.Long.compare; +import static java.util.stream.Collectors.toList; +import static org.eclipse.che.api.vfs.impl.file.event.HiEvent.Category.UNDEFINED; +import static org.eclipse.che.api.vfs.impl.file.event.HiEvent.Category.values; + +/** + * Splits high level event list according to their categories (see {@link Category}) + * and then broadcast categorized events with the highest priorities. + *

+ * Note: broadcasters are secured for each {@link HiEvent} instance during + * creation, so it is out of scope of this class to define broadcasters, but simply + * to call them. + *

+ * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@Beta +@Singleton +class HiEventBroadcasterManager { + void manageEvents(List hiEvents) { + if (hiEvents.isEmpty()) { + return; + } + for (Category category : values()) { + + final List events = hiEvents.stream() + .filter(o -> o.getCategory().equals(category)) + .sorted((o1, o2) -> compare(o1.getCategory().getPriority(), + o2.getCategory().getPriority())) + .collect(toList()); + + if (UNDEFINED.equals(category)) { + events.stream().forEach(HiEvent::broadcast); + } else if (!events.isEmpty()) { + // getting the event with the highest priority + events.get(0).broadcast(); + } + } + } + +} diff --git a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventClientBroadcaster.java b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventClientBroadcaster.java new file mode 100644 index 0000000000..707d9b3058 --- /dev/null +++ b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventClientBroadcaster.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import com.google.common.annotations.Beta; + +import org.eclipse.che.dto.server.DtoFactory; +import org.everrest.websockets.WSConnectionContext; +import org.everrest.websockets.message.ChannelBroadcastMessage; +import org.slf4j.Logger; + +import javax.inject.Singleton; +import javax.websocket.EncodeException; +import java.io.IOException; + +import static org.slf4j.LoggerFactory.getLogger; + +/** + * Designed to broadcast high level events over web sockets to a client-side listener. + * WS listener must beforehand know corresponding WE channel name and event DTO class, + * because basically all this class does is broadcasting a DTO to a specific channel. + * Nothing more. + *

+ * Note: Proper DTO object and web socket channel name are assumed to be stored in + * {@link HiEvent} instance passed to this broadcaster. Relevance of DTO and ws + * chanel data is the responsibility of {@link HiEvent} instance creator. + *

+ * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@Beta +@Singleton +public class HiEventClientBroadcaster implements HiEventBroadcaster { + private static final Logger LOG = getLogger(HiEventClientBroadcaster.class); + + @Override + public void broadcast(HiEvent event) { + final ChannelBroadcastMessage bm = new ChannelBroadcastMessage(); + bm.setChannel(event.getChannel()); + bm.setBody(DtoFactory.getInstance().toJson(event.getDto())); + + try { + WSConnectionContext.sendMessage(bm); + LOG.debug("Sending message over websocket connection: {}", bm); + } catch (EncodeException | IOException e) { + LOG.error("Can't send a VFS notification over web socket. Event: {}", event, e); + } + } +} diff --git a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventDetector.java b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventDetector.java new file mode 100644 index 0000000000..f383c3d11a --- /dev/null +++ b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventDetector.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import com.google.common.annotations.Beta; + +import java.util.Optional; + +/** + * Event detectors are the key components of the whole VFS event handling subsystem. + *

+ * The main purpose of each event detector is to analyze instances of + * {@link EventTreeNode} and to find out if a specific conditions are met. + * If yes - corresponding high level event should be generated. Besides the + * obvious (event detection), event detectors are also responsible for high + * level event generation. Those events should correspond to a specific event + * tree state and populated with relevant data. + *

+ *

+ * Perhaps those functions will be shared between detectors and event builders + * in future implementations. + *

+ *

+ * To omit details, it defines a way a set of low level events transforms into a + * specific high level event or a list of events. + *

+ *

+ * Note: Though it is not restricted to generate different high level events in + * correspondence to different event trees by a single detector, it is not + * recommended to do so to avoid complications in understanding and handling of + * events. Adhere to a rule: one detector - one event. Unless you know what you + * are doing. + *

+ * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@Beta +@FunctionalInterface +public interface HiEventDetector { + Optional> detect(EventTreeNode eventTreeNode); +} diff --git a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventDetectorManager.java b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventDetectorManager.java new file mode 100644 index 0000000000..a124f63693 --- /dev/null +++ b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventDetectorManager.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import com.google.common.annotations.Beta; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static java.util.stream.Collectors.toList; + +/** + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@Beta +@Singleton +class HiEventDetectorManager { + private final Set> hiEventDetectors; + + @Inject + public HiEventDetectorManager(Set> hiEventDetectors) { + this.hiEventDetectors = hiEventDetectors; + } + + List getDetectedEvents(EventTreeNode root) { + return hiEventDetectors.stream() + .map(o -> o.detect(root)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(toList()); + } + +} diff --git a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventServerPublisher.java b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventServerPublisher.java new file mode 100644 index 0000000000..b451d00a05 --- /dev/null +++ b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventServerPublisher.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import com.google.common.annotations.Beta; + +import org.eclipse.che.api.core.notification.EventService; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import static org.slf4j.LoggerFactory.getLogger; + +/** + * Event server-side publisher with a very basic set of functionality. + * It simply publishes the events DTO to an instance of {@link EventService} + *

+ * Note: DTO object is assumed to be stored in {@link HiEvent} instance + * passed to this broadcaster. + *

+ * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@Beta +@Singleton +public class HiEventServerPublisher implements HiEventBroadcaster { + private static final Logger LOG = getLogger(HiEventClientBroadcaster.class); + + private final EventService eventService; + + @Inject + public HiEventServerPublisher(EventService eventService) { + this.eventService = eventService; + } + + @Override + public void broadcast(HiEvent event) { + eventService.publish(event.getDto()); + LOG.debug("Publishing event to event service: {}.", event.getDto()); + } +} diff --git a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventService.java b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventService.java new file mode 100644 index 0000000000..fdf1488021 --- /dev/null +++ b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/HiEventService.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import com.google.common.annotations.Beta; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.List; +import java.util.Optional; + +import static org.slf4j.LoggerFactory.getLogger; + +/** + * The main mission of this service is to manage event trees produced by {@link LoEventService}. + * It is done with the help of {@link HiEventDetectorManager} and {@link HiEventBroadcasterManager}. + * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@Beta +@Singleton +public class HiEventService extends VfsEventService { + private final HiEventDetectorManager hiEventDetectorManager; + private final HiEventBroadcasterManager hiEventBroadcasterManager; + private final EventTreeQueueHolder eventTreeQueueHolder; + + @Inject + public HiEventService(HiEventDetectorManager hiEventDetectorManager, + EventTreeQueueHolder eventTreeQueueHolder, + HiEventBroadcasterManager hiEventBroadcasterManager) { + this.hiEventDetectorManager = hiEventDetectorManager; + this.eventTreeQueueHolder = eventTreeQueueHolder; + this.hiEventBroadcasterManager = hiEventBroadcasterManager; + } + + protected void run() { + final Optional optional = eventTreeQueueHolder.take(); + if (optional.isPresent()) { + final EventTreeNode treeRoot = optional.get(); + final List events = hiEventDetectorManager.getDetectedEvents(treeRoot); + hiEventBroadcasterManager.manageEvents(events); + } + } +} diff --git a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/LoEvent.java b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/LoEvent.java new file mode 100644 index 0000000000..7fcbe3712c --- /dev/null +++ b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/LoEvent.java @@ -0,0 +1,117 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import com.google.common.annotations.Beta; + +import org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType; +import org.eclipse.che.api.vfs.VirtualFileSystem; +import org.eclipse.che.api.vfs.impl.file.FileTreeWatcher; +import org.eclipse.che.api.vfs.impl.file.FileWatcherNotificationListener; + +/** + * Low level virtual file system event implementation designed to represent + * trivial file system items manipulations ({@link FileWatcherEventType}). + * Roughly speaking it stands for physical file system operations (create file, + * edit file, create folder, etc.). + * + * *

+ * It is used for both file and folder related events ({@link ItemType}) and + * contains all relevant data (time, path, event and item types). + *

+ *

+ * Note: {@link LoEvent#getTime()} and {@link LoEvent#withTime(long)} methods + * are designed to deal with the time that corresponds to the moment we caught + * the event inside the application, not the real item modification time that + * comes from underlying file system. This is due to the fact that most file + * systems support timestamps accuracy within one second which is not enough + * for our purposes. + *

+ * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@Beta +public class LoEvent { + + /** + * Time in milliseconds to represent the moment VFS event is fired by + * {@link FileTreeWatcher} and caught by one of {@link FileWatcherNotificationListener} + * implementations + */ + private long time; + /** + * Absolute item path in terms of {@link VirtualFileSystem}. It is expected + * that this path starts from root position of file system. In common sense + * it is the folder obtained by {@link VirtualFileSystem#getRoot()} method. + */ + private String path; + private String name; + private FileWatcherEventType eventType; + private ItemType itemType; + + private LoEvent() { + } + + public static LoEvent newInstance() { + return new LoEvent(); + } + + public ItemType getItemType() { + return itemType; + } + + public LoEvent withItemType(ItemType itemType) { + this.itemType = itemType; + return this; + } + + public long getTime() { + return time; + } + + public LoEvent withTime(long time) { + this.time = time; + return this; + } + + public String getPath() { + return path; + } + + public LoEvent withPath(String path) { + this.path = path; + return this; + } + + public String getName() { + return name; + } + + public LoEvent withName(String name) { + this.name = name; + return this; + } + + public FileWatcherEventType getEventType() { + return eventType; + } + + public LoEvent withEventType(FileWatcherEventType eventType) { + this.eventType = eventType; + return this; + } + + public enum ItemType { + FILE, DIR, UNDEFINED + } +} diff --git a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/LoEventListener.java b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/LoEventListener.java new file mode 100644 index 0000000000..e7fc3fc979 --- /dev/null +++ b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/LoEventListener.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import com.google.common.annotations.Beta; + +import org.eclipse.che.api.core.notification.EventService; +import org.eclipse.che.api.core.notification.EventSubscriber; +import org.slf4j.Logger; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.inject.Inject; +import javax.inject.Singleton; + +import static org.slf4j.LoggerFactory.getLogger; + +/** + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@Beta +@Singleton +public class LoEventListener implements EventSubscriber { + private static final Logger LOG = getLogger(LoEventListener.class); + + private final EventService eventService; + private final LoEventQueueHolder loEventQueueHolder; + + @Inject + public LoEventListener(EventService eventService, LoEventQueueHolder loEventQueueHolder) { + this.eventService = eventService; + this.loEventQueueHolder = loEventQueueHolder; + } + + @PostConstruct + void postConstruct() { + eventService.subscribe(this); + LOG.info("Subscribing to event service: {}", this.getClass()); + } + + @PreDestroy + void preDestroy() { + eventService.unsubscribe(this); + LOG.info("Unsubscribing to event service: {}", this.getClass()); + } + + @Override + public void onEvent(LoEvent event) { + loEventQueueHolder.put(event); + LOG.trace("Putting event {} to {}", event, LoEventQueueHolder.class); + } +} diff --git a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/LoEventQueueHolder.java b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/LoEventQueueHolder.java new file mode 100644 index 0000000000..cbc46f6a23 --- /dev/null +++ b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/LoEventQueueHolder.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import com.google.common.annotations.Beta; + +import org.slf4j.Logger; + +import javax.inject.Singleton; +import java.util.Optional; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import static java.util.Optional.empty; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.slf4j.LoggerFactory.getLogger; + +/** + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@Beta +@Singleton +class LoEventQueueHolder { + private static final Logger LOG = getLogger(LoEventQueueHolder.class); + + private final BlockingQueue loEventQueue; + + public LoEventQueueHolder() { + this.loEventQueue = new LinkedBlockingQueue<>(); + } + + void put(LoEvent loEvent) { + try { + loEventQueue.put(loEvent); + } catch (InterruptedException e) { + LOG.error("Error trying to put an event to an event queue: {}", loEvent, e); + } + } + + Optional poll(long timeout) { + try { + return Optional.ofNullable(loEventQueue.poll(timeout, MILLISECONDS)); + } catch (InterruptedException e) { + LOG.error("Error trying to poll an event out of an event queue", e); + } + return empty(); + } +} diff --git a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/LoEventService.java b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/LoEventService.java new file mode 100644 index 0000000000..bb2def5883 --- /dev/null +++ b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/LoEventService.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import com.google.common.annotations.Beta; + +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Optional; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +import static org.eclipse.che.api.vfs.impl.file.event.EventTreeHelper.addEventAndCreatePrecedingNodes; +import static org.eclipse.che.api.vfs.impl.file.event.EventTreeNode.newRootInstance; +import static org.slf4j.LoggerFactory.getLogger; + +/** + * Main task of event low event service is to take low level events and process + * them into event tree according to their locations in a file system. The event + * tree is passed further to a event detectors and broadcasters managed by upper + * {@link HiEventService}. + * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@Beta +@Singleton +public class LoEventService extends VfsEventService { + private static final Logger LOG = getLogger(LoEventService.class); + + /** + * Maximal time interval between events that we consider them + * to belong a common event segment. + */ + static final long MAX_EVENT_INTERVAL_MILLIS = 300L; + /** + * Maximal time interval that an event segment can last. This is used + * to avoid long going events sequences. We are splitting them into several + * lesser. + */ + static final long MAX_TIME_SEGMENT_SIZE_MILLIS = 5 * MAX_EVENT_INTERVAL_MILLIS; + + /** + * This constant is used to set undefined timestamp in case if a new event + * segment is not started but the event interval is big enough to finish + * previous event segment and to enqueue an old tree. + */ + private static final long UNDEFINED = -1L; + + private final LoEventQueueHolder loEventQueueHolder; + private final EventTreeQueueHolder eventTreeQueueHolder; + + private EventTreeNode vfsEventTreeRoot; + private long eventSegmentStartTime; + + @Inject + public LoEventService(LoEventQueueHolder loEventQueueHolder, + EventTreeQueueHolder eventTreeQueueHolder) { + this.loEventQueueHolder = loEventQueueHolder; + this.eventTreeQueueHolder = eventTreeQueueHolder; + + this.vfsEventTreeRoot = newRootInstance(); + this.eventSegmentStartTime = UNDEFINED; + + } + + @Override + protected void run() { + Optional optional = loEventQueueHolder.poll(MAX_EVENT_INTERVAL_MILLIS); + + if (optional.isPresent()) { + final LoEvent loEvent = optional.get(); + final long eventTime = loEvent.getTime(); + + if (eventSegmentStartTime == UNDEFINED || eventTime - eventSegmentStartTime >= MAX_TIME_SEGMENT_SIZE_MILLIS) { + LOG.trace("Starting new event segment."); + LOG.trace("Old event segment start time: {} ", eventSegmentStartTime); + LOG.trace("New event segment start time: {} ", eventTime); + + flushOldTreeAndStartNew(); + eventSegmentStartTime = eventTime; + } + + addEventAndCreatePrecedingNodes(vfsEventTreeRoot, loEvent); + } else { + flushOldTreeAndStartNew(); + eventSegmentStartTime = UNDEFINED; + } + } + + private void flushOldTreeAndStartNew() { + if (vfsEventTreeRoot.getChildren().isEmpty()) { + return; + } + + eventTreeQueueHolder.put(vfsEventTreeRoot); + LOG.trace("Flushing old event tree {}.", vfsEventTreeRoot); + + vfsEventTreeRoot = newRootInstance(); + LOG.trace("Starting new event tree {}.", vfsEventTreeRoot); + } +} diff --git a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/PomModifiedHiEventDetector.java b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/PomModifiedHiEventDetector.java new file mode 100644 index 0000000000..b7df57bfef --- /dev/null +++ b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/PomModifiedHiEventDetector.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import com.google.common.annotations.Beta; +import com.google.inject.Inject; + +import org.eclipse.che.api.project.shared.dto.event.PomModifiedEventDto; + +import java.util.Optional; + +import static java.util.Optional.empty; +import static org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType.MODIFIED; +import static org.eclipse.che.api.vfs.impl.file.event.HiEvent.Category.PROJECT_INFRASTRUCTURE; +import static org.eclipse.che.dto.server.DtoFactory.newDto; + +/** + * Detects if any of maven project object model files is MODIFIED during previous + * event segment. If there are more than one modified file, generate event for the + * highest (in context of file system location hierarchy) spotted. + * + *

+ * Note: this implementation deals only with standard project object model file + * names - {@code pom.xml}. So if it is necessary to support custom naming you + * can extend this class. + *

+ * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@Beta +public class PomModifiedHiEventDetector implements HiEventDetector { + + private static final String POM_XML = "pom.xml"; + + private HiEventBroadcaster broadcaster; + + @Inject + public PomModifiedHiEventDetector(HiEventServerPublisher broadcaster) { + this.broadcaster = broadcaster; + } + + @Override + public Optional> detect(EventTreeNode eventTreeNode) { + if (!eventTreeNode.isRoot() || eventTreeNode.getChildren().isEmpty()) { + return empty(); + } + + final Optional highestPom = eventTreeNode.stream() + .filter(EventTreeNode::modificationOccurred) + .filter(EventTreeNode::isFile) + .filter(event -> POM_XML.equals(event.getName())) + .filter(event -> MODIFIED.equals(event.getLastEventType())) + // note the revers order of o1 and o2 + .sorted((o1, o2) -> o2.getPath().compareTo(o1.getPath())) + .findFirst(); + + if (!highestPom.isPresent()) { + return empty(); + } + + PomModifiedEventDto dto = newDto(PomModifiedEventDto.class).withPath(highestPom.get().getPath()); + + return Optional.of((HiEvent.newInstance(PomModifiedEventDto.class) + .withCategory(PROJECT_INFRASTRUCTURE.withPriority(50)) + .withBroadcaster(broadcaster) + .withDto(dto))); + } +} diff --git a/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/VfsEventService.java b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/VfsEventService.java new file mode 100644 index 0000000000..f0d66615b9 --- /dev/null +++ b/wsagent/che-core-api-project/src/main/java/org/eclipse/che/api/vfs/impl/file/event/VfsEventService.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import com.google.common.annotations.Beta; +import com.google.common.util.concurrent.ThreadFactoryBuilder; + +import org.slf4j.Logger; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicBoolean; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static java.util.concurrent.Executors.newSingleThreadExecutor; +import static org.slf4j.LoggerFactory.getLogger; + +/** + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@Beta +abstract class VfsEventService { + private static final Logger LOG = getLogger(VfsEventService.class); + + private final ExecutorService executor; + private final AtomicBoolean running; + + VfsEventService() { + final String threadName = getClass().getSimpleName().concat("Thread-%d"); + final ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat(threadName) + .setDaemon(TRUE) + .build(); + + this.executor = newSingleThreadExecutor(threadFactory); + this.running = new AtomicBoolean(FALSE); + } + + @PostConstruct + protected void postConstruct() { + running.compareAndSet(FALSE, TRUE); + executor.execute(this::runAsASingleThread); + LOG.info("Starting virtual file system event service: {}", this.getClass().getSimpleName()); + } + + @PreDestroy + protected void preDestroy() { + running.compareAndSet(TRUE, FALSE); + executor.shutdown(); + LOG.info("Stopping virtual file system event service: {}", this.getClass().getSimpleName()); + } + + private void runAsASingleThread() { + while (running.get()) { + run(); + } + } + + protected abstract void run(); +} diff --git a/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/EventTreeNodeTest.java b/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/EventTreeNodeTest.java new file mode 100644 index 0000000000..c86c543d48 --- /dev/null +++ b/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/EventTreeNodeTest.java @@ -0,0 +1,99 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType; +import org.junit.Before; +import org.junit.Test; + +import java.util.Optional; + +import static java.io.File.separator; +import static org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType.CREATED; +import static org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType.MODIFIED; +import static org.eclipse.che.api.vfs.impl.file.event.LoEvent.ItemType.FILE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Test for {@link EventTreeNode} + * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +public class EventTreeNodeTest { + + private static final String TEST_PATH = separator + "che" + separator + "file"; + private static final String CHILD_NAME_1 = "Child 1"; + private static final String CHILD_NAME_2 = "Child 2"; + + private EventTreeNode root; + private EventTreeNode child; + private EventTreeNode anotherChild; + + @Before + public void setUp() { + root = EventTreeNode.newRootInstance(); + child = EventTreeNode.newInstance(); + anotherChild = EventTreeNode.newInstance(); + } + + @Test + public void shouldProperlyAddChild() { + root.withChild(child); + + final Optional optional = root.getFirstChild(); + + assertTrue(optional.isPresent()); + assertEquals(optional.get(), child); + assertEquals(1, root.getChildren().size()); + } + + @Test + public void shouldProperlyAddEvent() { + long time = System.currentTimeMillis(); + + assertTrue(child.getEvents().isEmpty()); + + child.withEvent(getLoVfsEvent(time, CREATED)); + + assertEquals(1, child.getEvents().size()); + assertNotNull(child.getEvents().get(time)); + } + + @Test + public void shouldProperlyGetFirstChild() { + root.withChild(child.withName(CHILD_NAME_1)); + root.withChild(anotherChild.withName(CHILD_NAME_2)); + + assertTrue(root.getFirstChild().isPresent()); + assertEquals(CHILD_NAME_1, root.getFirstChild().get().getName()); + } + + @Test + public void shouldProperlyGetLastEventType() { + child.withEvent(getLoVfsEvent(System.currentTimeMillis(), CREATED)); + child.withEvent(getLoVfsEvent(System.currentTimeMillis(), MODIFIED)); + + assertEquals(MODIFIED, child.getLastEventType()); + } + + private LoEvent getLoVfsEvent(long time, FileWatcherEventType type) { + return LoEvent.newInstance() + .withPath(TEST_PATH) + .withTime(time) + .withItemType(FILE) + .withEventType(type); + } + +} diff --git a/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/GitCheckoutHiEventDetectorTest.java b/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/GitCheckoutHiEventDetectorTest.java new file mode 100644 index 0000000000..876fb61282 --- /dev/null +++ b/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/GitCheckoutHiEventDetectorTest.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import org.eclipse.che.api.project.shared.dto.event.GitBranchCheckoutEventDto; +import org.eclipse.che.api.vfs.VirtualFile; +import org.eclipse.che.api.vfs.VirtualFileSystem; +import org.eclipse.che.api.vfs.VirtualFileSystemProvider; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.io.File; +import java.util.Optional; + +import static org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType.CREATED; +import static org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType.MODIFIED; +import static org.eclipse.che.api.vfs.impl.file.event.LoEvent.ItemType.FILE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Test for {@link GitCheckoutHiEventDetector} + * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@RunWith(MockitoJUnitRunner.class) +public class GitCheckoutHiEventDetectorTest extends HiVfsEventDetectorTestHelper { + + private static final String PROJECT_NAME = "project"; + private static final String HEAD_FILE_PATH = ".git/HEAD"; + private static final String TEST_VALUE = "TEST VALUE"; + private static final String HEAD_FILE_CONTENT = "ref: refs" + File.separator + "heads" + File.separator + TEST_VALUE; + private static final String HEAD_FILE_NAME = "HEAD"; + private static final String GIT_OPERATIONS_CHANNEL = "git-operations-channel"; + + @Mock + private HiEventClientBroadcaster hiVfsEventClientBroadcaster; + @Mock + private VirtualFileSystemProvider virtualFileSystemProvider; + + private GitCheckoutHiEventDetector gitCheckoutHiVfsEventDetector; + + @Before + public void setUp() throws Exception { + super.setUp(); + gitCheckoutHiVfsEventDetector = new GitCheckoutHiEventDetector(virtualFileSystemProvider, hiVfsEventClientBroadcaster); + } + + @Test + public void shouldReturnEmptyListBecauseNoHeadFileModified() { + assertFalse(gitCheckoutHiVfsEventDetector.detect(root).isPresent()); + } + + @Test + public void shouldReturnEmptyListBecauseOfWrongHeadFileModification() { + addEvent(HEAD_FILE_NAME, File.separator + PROJECT_NAME + File.separator + HEAD_FILE_PATH, CREATED, FILE); + + assertFalse(gitCheckoutHiVfsEventDetector.detect(root).isPresent()); + } + + @Test + public void shouldReturnListWithCorrectEventBecauseHeadFileIsProperlyModified() throws Exception { + VirtualFileSystem vfs = mock(VirtualFileSystem.class); + VirtualFile file = mock(VirtualFile.class); + + when(virtualFileSystemProvider.getVirtualFileSystem()).thenReturn(vfs); + when(vfs.getRoot()).thenReturn(file); + when(file.getChild(any())).thenReturn(file); + when(file.getContentAsString()).thenReturn(HEAD_FILE_CONTENT); + + addEvent(HEAD_FILE_NAME, File.separator + PROJECT_NAME + File.separator + HEAD_FILE_PATH, MODIFIED, FILE); + + Optional> optional = gitCheckoutHiVfsEventDetector.detect(root); + assertTrue(optional.isPresent()); + + HiEvent hiEvent = optional.get(); + assertEquals(GIT_OPERATIONS_CHANNEL, hiEvent.getChannel()); + assertEquals(TEST_VALUE, hiEvent.getDto().getBranchName()); + } +} diff --git a/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/HiEventServiceTest.java b/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/HiEventServiceTest.java new file mode 100644 index 0000000000..80d83191a3 --- /dev/null +++ b/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/HiEventServiceTest.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.runners.MockitoJUnitRunner; + +import static java.lang.Thread.sleep; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Test for {@link HiEventService} + * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@RunWith(MockitoJUnitRunner.class) +public class HiEventServiceTest { + @Mock + private HiEventDetectorManager hiEventDetectorManager; + @Mock + private HiEventBroadcasterManager hiEventBroadcasterManager; + @Spy + private EventTreeQueueHolder eventTreeQueueHolder; + @InjectMocks + private HiEventService hiEventService; + + @Before + public void setUp() throws Exception { + hiEventService.postConstruct(); + } + + @After + public void tearDown() throws Exception { + hiEventService.preDestroy(); + } + + @Test + public void shouldInvokeAllComponents() throws Exception { + final EventTreeNode eventTreeNode = mock(EventTreeNode.class); + + eventTreeQueueHolder.put(eventTreeNode); + + sleep(1000); + + verify(eventTreeQueueHolder, atLeastOnce()).take(); + verify(hiEventDetectorManager).getDetectedEvents(eq(eventTreeNode)); + verify(hiEventBroadcasterManager).manageEvents(any()); + } + +} diff --git a/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/HiVfsEventDetectorTestHelper.java b/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/HiVfsEventDetectorTestHelper.java new file mode 100644 index 0000000000..e06fc5d6b9 --- /dev/null +++ b/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/HiVfsEventDetectorTestHelper.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType; +import org.eclipse.che.api.vfs.impl.file.event.LoEvent.ItemType; + +import static org.eclipse.che.api.vfs.impl.file.event.EventTreeHelper.addEventAndCreatePrecedingNodes; +import static org.eclipse.che.api.vfs.impl.file.event.EventTreeNode.newRootInstance; +import static org.eclipse.che.api.vfs.impl.file.event.LoEvent.newInstance; + +/** + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +abstract class HiVfsEventDetectorTestHelper { + + protected EventTreeNode root; + + public void setUp() throws Exception { + root = newRootInstance(); + } + + void addEvent(String name, + String path, + FileWatcherEventType eventType, + ItemType itemType) { + + LoEvent loEvent = newInstance() + .withName(name) + .withPath(path) + .withEventType(eventType) + .withItemType(itemType) + .withTime(System.currentTimeMillis()); + + addEventAndCreatePrecedingNodes(root, loEvent); + } + +} diff --git a/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/LoEventListenerTest.java b/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/LoEventListenerTest.java new file mode 100644 index 0000000000..a06d95c5ac --- /dev/null +++ b/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/LoEventListenerTest.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + + +import org.eclipse.che.api.core.notification.EventService; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link LoEventListener} + * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@RunWith(MockitoJUnitRunner.class) +public class LoEventListenerTest { + @Spy + private EventService eventService; + @Mock + private LoEventQueueHolder loEventQueueHolder; + @InjectMocks + private LoEventListener loEventListener; + + @Before + public void setUp() throws Exception { + loEventListener.postConstruct(); + verify(eventService).subscribe(any(LoEventListener.class)); + } + + @After + public void tearDown() throws Exception { + loEventListener.preDestroy(); + verify(eventService).unsubscribe(isA(LoEventListener.class)); + } + + @Test + public void shouldEnqueuePublishedVfsEvent() throws Exception { + final LoEvent loEvent = mock(LoEvent.class); + + eventService.publish(loEvent); + verify(loEventQueueHolder).put(eq(loEvent)); + } + + @Test + public void shouldNotEnqueuePublishedNonVfsEvent() throws Exception { + eventService.publish(new Object()); + verifyZeroInteractions(loEventQueueHolder); + } + +} diff --git a/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/LoEventServiceTest.java b/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/LoEventServiceTest.java new file mode 100644 index 0000000000..91a35d3aee --- /dev/null +++ b/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/LoEventServiceTest.java @@ -0,0 +1,191 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.Optional; + +import static java.io.File.separator; +import static java.lang.System.currentTimeMillis; +import static java.lang.Thread.sleep; +import static org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType.MODIFIED; +import static org.eclipse.che.api.vfs.impl.file.event.LoEvent.ItemType.FILE; +import static org.eclipse.che.api.vfs.impl.file.event.LoEvent.newInstance; +import static org.eclipse.che.api.vfs.impl.file.event.LoEventService.MAX_EVENT_INTERVAL_MILLIS; +import static org.eclipse.che.api.vfs.impl.file.event.LoEventService.MAX_TIME_SEGMENT_SIZE_MILLIS; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Test for {@link LoEventService} + * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@RunWith(MockitoJUnitRunner.class) +public class LoEventServiceTest { + private static final long TIMEOUT = 2 * MAX_EVENT_INTERVAL_MILLIS; + private static final String FOLDER_NAME = "folder"; + private static final String FILE_NAME = "file"; + private static final String PATH = separator + FOLDER_NAME + separator + FILE_NAME; + + @Spy + private LoEventQueueHolder loEventQueueHolder; + @Spy + private EventTreeQueueHolder eventTreeQueueHolder; + @InjectMocks + private LoEventService loEventService; + + @Before + public void setUp() throws Exception { + loEventService.postConstruct(); + } + + @After + public void tearDown() throws Exception { + loEventService.preDestroy(); + } + + @Test + public void shouldConsumeSimpleEventSegmentAndProduceEventTree() throws Exception { + final LoEvent loEvent = getLoEvent(PATH, FILE_NAME); + loEventQueueHolder.put(loEvent); + final Optional rootOptional = eventTreeQueueHolder.take(); + + assertNotNull(rootOptional); + assertTrue(rootOptional.isPresent()); + + final EventTreeNode root = rootOptional.get(); + assertTrue(root.isRoot()); + assertFalse(root.modificationOccurred()); + + final Optional dirOptional = root.getChild(FOLDER_NAME); + assertNotNull(dirOptional); + assertTrue(dirOptional.isPresent()); + + final EventTreeNode dir = dirOptional.get(); + assertEquals(1, dir.getChildren().size()); + assertFalse(dir.modificationOccurred()); + + final Optional fileOptional = dir.getChild(FILE_NAME); + + assertNotNull(fileOptional); + assertTrue(fileOptional.isPresent()); + + final EventTreeNode file = fileOptional.get(); + assertTrue(file.modificationOccurred()); + assertEquals(MODIFIED, file.getLastEventType()); + assertTrue(file.isFile()); + assertEquals(PATH, file.getPath()); + assertEquals(FILE_NAME, file.getName()); + } + + @Test + public void shouldConsumeTwoEventSegmentsAndProduceTwoEventTries() throws Exception { + final LoEvent loEventI = getLoEvent(separator + FOLDER_NAME + 1 + separator + FILE_NAME + 1, FILE_NAME + 1); + loEventQueueHolder.put(loEventI); + final Optional rootOptionalI = eventTreeQueueHolder.take(); + + sleep(TIMEOUT); // this is required to properly simulate delay between events, should exceed max event interval + + final LoEvent loEventII = getLoEvent(separator + FOLDER_NAME + 2 + separator + FILE_NAME + 2, FILE_NAME + 2); + loEventQueueHolder.put(loEventII); + final Optional rootOptionalII = eventTreeQueueHolder.take(); + + assertNotNull(rootOptionalI); + assertTrue(rootOptionalI.isPresent()); + assertNotNull(rootOptionalII); + assertTrue(rootOptionalII.isPresent()); + + final EventTreeNode rootI = rootOptionalI.get(); + final EventTreeNode rootII = rootOptionalII.get(); + + assertEquals(1, rootI.getChildren().size()); + assertEquals(1, rootII.getChildren().size()); + + final Optional folderOptionalI = rootI.getChild(FOLDER_NAME + 1); + final Optional folderOptionalII = rootII.getChild(FOLDER_NAME + 2); + + assertNotNull(folderOptionalI); + assertTrue(folderOptionalI.isPresent()); + assertNotNull(folderOptionalI); + assertTrue(folderOptionalII.isPresent()); + + final EventTreeNode folderI = folderOptionalI.get(); + final EventTreeNode folderII = folderOptionalII.get(); + + assertEquals(1, folderI.getChildren().size()); + assertEquals(1, folderII.getChildren().size()); + + final Optional fileOptionalI = folderI.getChild(FILE_NAME + 1); + final Optional fileOptionalII = folderII.getChild(FILE_NAME + 2); + + assertNotNull(fileOptionalI); + assertTrue(fileOptionalI.isPresent()); + assertNotNull(fileOptionalII); + assertTrue(fileOptionalII.isPresent()); + + final EventTreeNode fileI = fileOptionalI.get(); + final EventTreeNode fileII = fileOptionalII.get(); + + assertEquals(FILE_NAME + 1, fileI.getName()); + assertEquals(FILE_NAME + 2, fileII.getName()); + } + + @Test + public void shouldCreateEventTreeBecauseOfExceedingEventSegmentTimeFrame() throws Exception { + final long sleepTime = 150; + final long start = currentTimeMillis(); + + long totalCounter = 0; + long firstSegmentCounter = 0; + + while ((currentTimeMillis() - start) < MAX_TIME_SEGMENT_SIZE_MILLIS + MAX_EVENT_INTERVAL_MILLIS) { + if ((firstSegmentCounter == 0) && ((currentTimeMillis() - start) > MAX_TIME_SEGMENT_SIZE_MILLIS)) { + firstSegmentCounter = totalCounter; + } + totalCounter++; + loEventQueueHolder.put(getLoEvent(PATH + totalCounter, FILE_NAME + totalCounter)); + sleep(sleepTime); + } + + final Optional rootOptionalI = eventTreeQueueHolder.take(); + final Optional rootOptionalII = eventTreeQueueHolder.take(); + assertTrue(rootOptionalI.isPresent()); + assertTrue(rootOptionalII.isPresent()); + + final Optional folderI = rootOptionalI.get().getFirstChild(); + final Optional folderII = rootOptionalII.get().getFirstChild(); + assertTrue(folderI.isPresent()); + assertTrue(folderII.isPresent()); + + assertEquals(firstSegmentCounter, folderI.get().getChildren().size()); + assertEquals(totalCounter - firstSegmentCounter, folderII.get().getChildren().size()); + } + + private LoEvent getLoEvent(String path, String name) { + return newInstance().withName(name) + .withPath(path) + .withEventType(MODIFIED) + .withItemType(FILE) + .withTime(currentTimeMillis()); + } +} diff --git a/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/LoEventTreeHelperTest.java b/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/LoEventTreeHelperTest.java new file mode 100644 index 0000000000..d0477ec0c9 --- /dev/null +++ b/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/LoEventTreeHelperTest.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import org.junit.Before; +import org.junit.Test; + +import java.util.Optional; + +import static java.io.File.separator; +import static org.eclipse.che.api.vfs.impl.file.event.EventTreeHelper.addEventAndCreatePrecedingNodes; +import static org.eclipse.che.api.vfs.impl.file.event.EventTreeHelper.getTreeNode; +import static org.eclipse.che.api.vfs.impl.file.event.EventTreeNode.newRootInstance; +import static org.eclipse.che.api.vfs.impl.file.event.LoEvent.newInstance; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * Test for {@link EventTreeHelper} + * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +public class LoEventTreeHelperTest { + private static final String CHE = "che"; + private static final String CHU = "chu"; + private static final String CHA = "cha"; + private static final String CHI = "chi"; + private static final String PATH = separator + CHE + separator + CHU + separator + CHA + separator + CHI; + + private EventTreeNode root; + + @Before + public void setUp() throws Exception { + root = newRootInstance(); + } + + @Test + public void shouldImplicitlyCreateAllAncestors() { + final LoEvent loEvent = newInstance().withPath(PATH); + addEventAndCreatePrecedingNodes(root, loEvent); + testNode(testNode(testNode(testNode(root, CHE), CHU), CHA), CHI); + } + + @Test + public void shouldGetExistingTreeNode() { + final LoEvent loEvent = newInstance().withName(CHI).withPath(PATH); + + addEventAndCreatePrecedingNodes(root, loEvent); + + final Optional nodeOptional = getTreeNode(root, PATH); + assertTrue(nodeOptional.isPresent()); + + final EventTreeNode node = nodeOptional.get(); + assertEquals(loEvent.getPath(), node.getPath()); + assertEquals(loEvent.getName(), node.getName()); + } + + @Test + public void shouldNotGetNonExistingTreeNode() { + assertFalse(getTreeNode(root, CHE).isPresent()); + } + + private EventTreeNode testNode(EventTreeNode parent, String name) { + final Optional nodeOptional = parent.getFirstChild(); + assertTrue(nodeOptional.isPresent()); + + final EventTreeNode node = nodeOptional.get(); + assertNotNull(node); + assertEquals(name, node.getName()); + + return node; + } +} diff --git a/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/PomModifiedHiEventDetectorTest.java b/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/PomModifiedHiEventDetectorTest.java new file mode 100644 index 0000000000..a9e9106c1c --- /dev/null +++ b/wsagent/che-core-api-project/src/test/java/org/eclipse/che/api/vfs/impl/file/event/PomModifiedHiEventDetectorTest.java @@ -0,0 +1,98 @@ +/******************************************************************************* + * Copyright (c) 2012-2016 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; + +import org.eclipse.che.api.project.shared.dto.event.PomModifiedEventDto; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.Optional; + +import static java.io.File.separator; +import static org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType.CREATED; +import static org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType.MODIFIED; +import static org.eclipse.che.api.vfs.impl.file.event.HiEvent.Category.PROJECT_INFRASTRUCTURE; +import static org.eclipse.che.api.vfs.impl.file.event.LoEvent.ItemType.DIR; +import static org.eclipse.che.api.vfs.impl.file.event.LoEvent.ItemType.FILE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Test for {@link PomModifiedHiEventDetector} + * + * @author Dmitry Kuleshov + * + * @since 4.5 + */ +@RunWith(MockitoJUnitRunner.class) +public class PomModifiedHiEventDetectorTest extends HiVfsEventDetectorTestHelper { + + private static final String CHE_PATH = separator + "che" + separator; + private static final String FOLDER_1 = CHE_PATH + "folder1" + separator; + private static final String PATH_2 = CHE_PATH + "folder2" + separator; + private static final String TEST = "test"; + private static final String POM_XML = "pom.xml"; + @Mock + private HiEventServerPublisher hiVfsEventServerPublisher; + + private PomModifiedHiEventDetector pomModifiedHiVfsEventDetector; + + @Before + public void setUp() throws Exception { + super.setUp(); + pomModifiedHiVfsEventDetector = new PomModifiedHiEventDetector(hiVfsEventServerPublisher); + } + + @Test + public void shouldReturnEmptyListBecauseNoPomFileFound() { + assertFalse(pomModifiedHiVfsEventDetector.detect(root).isPresent()); + } + + @Test + public void shouldReturnEmptyListBecauseNoRelevantPomFileFound() { + addEvent(POM_XML, FOLDER_1 + POM_XML, CREATED, FILE); + addEvent(POM_XML, PATH_2 + POM_XML, MODIFIED, DIR); + addEvent(TEST, CHE_PATH + POM_XML + separator + TEST, MODIFIED, FILE); + + assertFalse(pomModifiedHiVfsEventDetector.detect(root).isPresent()); + } + + @Test + public void shouldReturnListWithPomEvent() { + addEvent(POM_XML, FOLDER_1 + POM_XML, CREATED, FILE); + addEvent(POM_XML, FOLDER_1 + POM_XML, MODIFIED, FILE); + addEvent(POM_XML, PATH_2 + POM_XML, MODIFIED, DIR); + addEvent(TEST, CHE_PATH + POM_XML + separator + TEST, MODIFIED, FILE); + + final Optional> eventOptional = pomModifiedHiVfsEventDetector.detect(root); + assertTrue(eventOptional.isPresent()); + + final HiEvent hiEvent = eventOptional.get(); + assertEquals(PROJECT_INFRASTRUCTURE, hiEvent.getCategory()); + assertEquals(FOLDER_1 + POM_XML, hiEvent.getDto().getPath()); + } + + @Test + public void shouldReturnListWithPomWithHighestFsHierarchyPosition() { + addEvent(POM_XML, FOLDER_1 + POM_XML, MODIFIED, FILE); + addEvent(POM_XML, CHE_PATH + POM_XML, MODIFIED, FILE); + + final Optional> eventOptional = pomModifiedHiVfsEventDetector.detect(root); + assertTrue(eventOptional.isPresent()); + + final HiEvent hiEvent = eventOptional.get(); + assertEquals(CHE_PATH + POM_XML, hiEvent.getDto().getPath()); + } +}