/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.ide.command.manager;
import elemental.util.ArrayOf;
import elemental.util.Collections;
import com.google.gwt.core.client.Callback;
import com.google.gwt.core.client.Scheduler;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.web.bindery.event.shared.EventBus;
import org.eclipse.che.api.core.model.machine.Machine;
import org.eclipse.che.api.promises.client.Function;
import org.eclipse.che.api.promises.client.Promise;
import org.eclipse.che.api.promises.client.PromiseProvider;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.ide.api.app.AppContext;
import org.eclipse.che.ide.api.command.CommandAddedEvent;
import org.eclipse.che.ide.api.command.CommandImpl;
import org.eclipse.che.ide.api.command.CommandImpl.ApplicableContext;
import org.eclipse.che.ide.api.command.CommandManager;
import org.eclipse.che.ide.api.command.CommandRemovedEvent;
import org.eclipse.che.ide.api.command.CommandType;
import org.eclipse.che.ide.api.command.CommandTypeRegistry;
import org.eclipse.che.ide.api.command.CommandUpdatedEvent;
import org.eclipse.che.ide.api.command.CommandsLoadedEvent;
import org.eclipse.che.ide.api.component.WsAgentComponent;
import org.eclipse.che.ide.api.resources.Project;
import org.eclipse.che.ide.api.resources.Resource;
import org.eclipse.che.ide.api.selection.Selection;
import org.eclipse.che.ide.api.selection.SelectionAgent;
import org.eclipse.che.ide.util.loging.Log;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import static java.util.stream.Collectors.toList;
import static org.eclipse.che.api.workspace.shared.Constants.COMMAND_GOAL_ATTRIBUTE_NAME;
import static org.eclipse.che.api.workspace.shared.Constants.COMMAND_PREVIEW_URL_ATTRIBUTE_NAME;
/** Implementation of {@link CommandManager}. */
@Singleton
public class CommandManagerImpl implements CommandManager, WsAgentComponent {
private final AppContext appContext;
private final PromiseProvider promiseProvider;
private final CommandTypeRegistry commandTypeRegistry;
private final ProjectCommandManagerDelegate projectCommandManager;
private final WorkspaceCommandManagerDelegate workspaceCommandManager;
private final SelectionAgent selectionAgent;
private final EventBus eventBus;
private final CommandNameGenerator commandNameGenerator;
/** Map of the commands' names to the commands. */
private final Map<String, CommandImpl> commands;
@Inject
public CommandManagerImpl(AppContext appContext,
PromiseProvider promiseProvider,
CommandTypeRegistry commandTypeRegistry,
ProjectCommandManagerDelegate projectCommandManagerDelegate,
WorkspaceCommandManagerDelegate workspaceCommandManagerDelegate,
SelectionAgent selectionAgent,
EventBus eventBus,
CommandNameGenerator commandNameGenerator) {
this.appContext = appContext;
this.promiseProvider = promiseProvider;
this.commandTypeRegistry = commandTypeRegistry;
this.projectCommandManager = projectCommandManagerDelegate;
this.workspaceCommandManager = workspaceCommandManagerDelegate;
this.selectionAgent = selectionAgent;
this.eventBus = eventBus;
this.commandNameGenerator = commandNameGenerator;
commands = new HashMap<>();
}
@Override
public void start(Callback<WsAgentComponent, Exception> callback) {
fetchCommands(callback);
}
private void fetchCommands(Callback<WsAgentComponent, Exception> callback) {
// get all commands related to the workspace
workspaceCommandManager.getCommands(appContext.getWorkspaceId()).then(workspaceCommands -> {
workspaceCommands.forEach(workspaceCommand -> commands.put(workspaceCommand.getName(),
new CommandImpl(workspaceCommand, new ApplicableContext())));
// get all commands related to the projects
Arrays.stream(appContext.getProjects())
.forEach(project -> projectCommandManager.getCommands(project).forEach(projectCommand -> {
final CommandImpl existedCommand = commands.get(projectCommand.getName());
if (existedCommand == null) {
commands.put(projectCommand.getName(),
new CommandImpl(projectCommand, new ApplicableContext(project.getPath())));
} else {
if (projectCommand.equalsIgnoreContext(existedCommand)) {
existedCommand.getApplicableContext().addProject(project.getPath());
} else {
// normally, should never happen
Log.error(CommandManagerImpl.this.getClass(), "Different commands with the same names found");
}
}
}));
callback.onSuccess(this);
notifyCommandsLoaded();
});
}
@Override
public List<CommandImpl> getCommands() {
return commands.values()
.stream()
.map(CommandImpl::new)
.collect(toList());
}
@Override
public java.util.Optional<CommandImpl> getCommand(String name) {
return commands.values()
.stream()
.filter(command -> name.equals(command.getName()))
.findFirst();
}
@Override
public List<CommandImpl> getApplicableCommands() {
return commands.values()
.stream()
.filter(this::isCommandApplicable)
.map(CommandImpl::new)
.collect(toList());
}
@Override
public boolean isCommandApplicable(CommandImpl command) {
return isMachineSelected() || isCommandApplicableToCurrentProject(command);
}
/** Checks whether the machine is currently selected. */
private boolean isMachineSelected() {
final Selection<?> selection = selectionAgent.getSelection();
if (selection != null && !selection.isEmpty() && selection.isSingleSelection()) {
return selection.getHeadElement() instanceof Machine;
}
return false;
}
/** Checks whether the given command is applicable to the current project. */
private boolean isCommandApplicableToCurrentProject(CommandImpl command) {
final Set<String> applicableProjects = command.getApplicableContext().getApplicableProjects();
if (applicableProjects.isEmpty()) {
return true;
}
final Resource currentResource = appContext.getResource();
if (currentResource != null) {
final Project currentProject = currentResource.getProject();
if (currentProject != null) {
return applicableProjects.contains(currentProject.getPath());
}
}
return false;
}
@Override
public Promise<CommandImpl> createCommand(String goalId, String typeId) {
return createCommand(goalId,
typeId,
null,
null,
new HashMap<>(),
new ApplicableContext());
}
@Override
public Promise<CommandImpl> createCommand(String goalId, String typeId, ApplicableContext context) {
return createCommand(goalId, typeId, null, null, new HashMap<>(), context);
}
@Override
public Promise<CommandImpl> createCommand(String goalId,
String typeId,
String name,
String commandLine,
Map<String, String> attributes) {
return createCommand(goalId, typeId, name, commandLine, attributes, new ApplicableContext());
}
@Override
public Promise<CommandImpl> createCommand(String goalId,
String typeId,
@Nullable String name,
@Nullable String commandLine,
Map<String, String> attributes,
ApplicableContext context) {
final Optional<CommandType> commandType = commandTypeRegistry.getCommandTypeById(typeId);
if (!commandType.isPresent()) {
return promiseProvider.reject(new Exception("Unknown command type: '" + typeId + "'"));
}
final Map<String, String> attr = new HashMap<>(attributes);
attr.put(COMMAND_PREVIEW_URL_ATTRIBUTE_NAME, commandType.get().getPreviewUrlTemplate());
attr.put(COMMAND_GOAL_ATTRIBUTE_NAME, goalId);
return createCommand(new CommandImpl(commandNameGenerator.generate(typeId, name),
commandLine != null ? commandLine : commandType.get().getCommandLineTemplate(),
typeId,
attr,
context));
}
@Override
public Promise<CommandImpl> createCommand(CommandImpl command) {
return doCreateCommand(command).then((Function<CommandImpl, CommandImpl>)newCommand -> {
// postpone the notification because
// listeners should be notified after returning from #createCommand method
Scheduler.get().scheduleDeferred(() -> notifyCommandAdded(newCommand));
return newCommand;
});
}
/** Does the actual work for command creation. Doesn't notify listeners. */
private Promise<CommandImpl> doCreateCommand(CommandImpl command) {
final ApplicableContext context = command.getApplicableContext();
if (!context.isWorkspaceApplicable() && context.getApplicableProjects().isEmpty()) {
return promiseProvider.reject(new Exception("Command has to be applicable to the workspace or at least one project"));
}
final Optional<CommandType> commandType = commandTypeRegistry.getCommandTypeById(command.getType());
if (!commandType.isPresent()) {
return promiseProvider.reject(new Exception("Unknown command type: '" + command.getType() + "'"));
}
final CommandImpl newCommand = new CommandImpl(command);
newCommand.setName(commandNameGenerator.generate(command.getType(), command.getName()));
final ArrayOf<Promise<?>> commandPromises = Collections.arrayOf();
if (context.isWorkspaceApplicable()) {
Promise<CommandImpl> p = workspaceCommandManager.createCommand(newCommand)
.then((Function<CommandImpl, CommandImpl>)arg -> {
newCommand.getApplicableContext().setWorkspaceApplicable(true);
return newCommand;
});
commandPromises.push(p);
}
for (final String projectPath : context.getApplicableProjects()) {
final Project project = getProjectByPath(projectPath);
if (project == null) {
continue;
}
Promise<CommandImpl> p = projectCommandManager.createCommand(project, newCommand)
.then((Function<CommandImpl, CommandImpl>)arg -> {
newCommand.getApplicableContext().addProject(projectPath);
return newCommand;
});
commandPromises.push(p);
}
return promiseProvider.all2(commandPromises)
.then((Function<ArrayOf<?>, CommandImpl>)ignore -> {
commands.put(newCommand.getName(), newCommand);
return newCommand;
});
}
@Override
public Promise<CommandImpl> updateCommand(String name, CommandImpl commandToUpdate) {
final CommandImpl existedCommand = commands.get(name);
if (existedCommand == null) {
return promiseProvider.reject(new Exception("Command '" + name + "' does not exist."));
}
return doRemoveCommand(name).thenPromise(aVoid -> doCreateCommand(commandToUpdate)
.then((Function<CommandImpl, CommandImpl>)updatedCommand -> {
// listeners should be notified after returning from #updateCommand method
// so let's postpone notification
Scheduler.get().scheduleDeferred(() -> notifyCommandUpdated(existedCommand, updatedCommand));
return updatedCommand;
}));
}
@Override
public Promise<Void> removeCommand(String name) {
final CommandImpl command = commands.get(name);
if (command == null) {
return promiseProvider.reject(new Exception("Command '" + name + "' does not exist."));
}
return doRemoveCommand(name).then(aVoid -> {
// listeners should be notified after returning from #removeCommand method
// so let's postpone notification
Scheduler.get().scheduleDeferred(() -> notifyCommandRemoved(command));
});
}
/** Removes the command without notifying listeners. */
private Promise<Void> doRemoveCommand(String name) {
final CommandImpl command = commands.get(name);
if (command == null) {
return promiseProvider.reject(new Exception("Command '" + name + "' does not exist."));
}
final ApplicableContext context = command.getApplicableContext();
final ArrayOf<Promise<?>> commandPromises = Collections.arrayOf();
if (context.isWorkspaceApplicable()) {
commandPromises.push(workspaceCommandManager.removeCommand(name));
}
for (final String projectPath : context.getApplicableProjects()) {
final Project project = getProjectByPath(projectPath);
if (project == null) {
continue;
}
commandPromises.push(projectCommandManager.removeCommand(project, name));
}
return promiseProvider.all2(commandPromises)
.then((Function<ArrayOf<?>, Void>)arg -> {
commands.remove(command.getName());
return null;
});
}
@Nullable
private Project getProjectByPath(String path) {
for (Project project : appContext.getProjects()) {
if (path.equals(project.getPath())) {
return project;
}
}
return null;
}
private void notifyCommandsLoaded() {
eventBus.fireEvent(new CommandsLoadedEvent());
}
private void notifyCommandAdded(CommandImpl command) {
eventBus.fireEvent(new CommandAddedEvent(command));
}
private void notifyCommandRemoved(CommandImpl command) {
eventBus.fireEvent(new CommandRemovedEvent(command));
}
private void notifyCommandUpdated(CommandImpl prevCommand, CommandImpl command) {
eventBus.fireEvent(new CommandUpdatedEvent(prevCommand, command));
}
}