diff --git a/cli/src/main/java/com/box/l10n/mojito/cli/command/DropCancelCommand.java b/cli/src/main/java/com/box/l10n/mojito/cli/command/DropCancelCommand.java new file mode 100644 index 0000000000..19c5473cf2 --- /dev/null +++ b/cli/src/main/java/com/box/l10n/mojito/cli/command/DropCancelCommand.java @@ -0,0 +1,184 @@ +package com.box.l10n.mojito.cli.command; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.box.l10n.mojito.cli.command.param.Param; +import com.box.l10n.mojito.cli.console.Console; +import com.box.l10n.mojito.cli.console.ConsoleWriter; +import com.box.l10n.mojito.rest.client.DropClient; +import com.box.l10n.mojito.rest.entity.Drop; +import com.box.l10n.mojito.rest.entity.CancelDropConfig; +import com.box.l10n.mojito.rest.entity.PollableTask; +import com.box.l10n.mojito.rest.entity.Repository; +import org.fusesource.jansi.Ansi.Color; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Command to cancel a drop. Displays the list of drops available and ask the + * user the drop id to be cancelled. + * + * @author jaurambault, wadimw + */ +@Component +@Scope("prototype") +@Parameters(commandNames = {"drop-cancel"}, commandDescription = "Cancel an exported drop") +public class DropCancelCommand extends Command { + + /** + * logger + */ + static Logger logger = LoggerFactory.getLogger(DropCancelCommand.class); + + @Autowired + ConsoleWriter consoleWriter; + + @Parameter(names = {"--drop-id", "-i"}, arity = 1, required = false, description = "ID of a drop to cancel (skip drop fetching and process only given ID)") + Long dropId = null; + + @Parameter(names = {Param.REPOSITORY_LONG, Param.REPOSITORY_SHORT}, arity = 1, required = true, description = Param.REPOSITORY_DESCRIPTION) + String repositoryParam; + + @Parameter(names = {"--number-drops-fetched"}, arity = 1, required = false, description = "Number of drops fetched") + Long numberOfDropsToFetchParam = 10L; + + @Parameter(names = {"--show-all", "-all"}, required = false, description = "Show all drops (already imported drops are hidden by default)") + Boolean alsoShowImportedParam = false; + + @Autowired + CommandHelper commandHelper; + + @Autowired + Console console; + + @Autowired + DropClient dropClient; + + @Override + public void execute() throws CommandException { + if (dropId != null) { + cancelSpecifiedDrop(); + } else { + cancelDropsSelectingFromAvailableList(); + } + consoleWriter.newLine().fg(Color.GREEN).a("Finished").println(2); + } + + private void cancelSpecifiedDrop() throws CommandException { + consoleWriter.newLine().a("Cancel drop: ").fg(Color.CYAN).a(dropId).println(2); + + CancelDropConfig cancelDropConfig = dropClient.cancelDrop(dropId); + PollableTask task = cancelDropConfig.getPollableTask(); + + commandHelper.waitForPollableTask(task.getId()); + } + + private void cancelDropsSelectingFromAvailableList() throws CommandException { + Repository repository = commandHelper.findRepositoryByName(repositoryParam); + Map numberedAvailableDrops = getNumberedAvailableDrops(repository.getId()); + + if (numberedAvailableDrops.isEmpty()) { + consoleWriter.newLine().a("No drop available").println(); + } else { + consoleWriter.newLine().a("Drops available").println(); + + logger.debug("Display drops information"); + for (Map.Entry entry : numberedAvailableDrops.entrySet()) { + + Drop drop = entry.getValue(); + + consoleWriter.a(" ").fg(Color.CYAN).a(entry.getKey()).reset(). + a(" - id: ").fg(Color.MAGENTA).a(drop.getId()).reset(). + a(", name: ").fg(Color.MAGENTA).a(drop.getName()).reset(); + + if (Boolean.TRUE.equals(drop.getCanceled())) { + consoleWriter.fg(Color.GREEN).a(" CANCELED"); + } else if (drop.getLastImportedDate() == null) { + consoleWriter.fg(Color.GREEN).a(" NEW"); + } else { + consoleWriter.a(", last import: ").fg(Color.MAGENTA).a(drop.getLastImportedDate()); + } + + consoleWriter.println(); + } + + List dropIds = getSelectedDropIds(numberedAvailableDrops); + + for(Long dropId: dropIds) { + consoleWriter.newLine().a("Cancel drop: ").fg(Color.CYAN).a(dropId).reset().a(" in repository: ").fg(Color.CYAN).a(repositoryParam).println(2); + + CancelDropConfig cancelDropConfig = dropClient.cancelDrop(dropId); + PollableTask pollableTask = cancelDropConfig.getPollableTask(); + + commandHelper.waitForPollableTask(pollableTask.getId()); + } + } + } + + /** + * Gets available {@link Drop}s and assign them a number (map key) to be + * referenced in the console input for selection. + * + * @return + */ + private Map getNumberedAvailableDrops(Long repositoryId) { + + logger.debug("Build a map of drops keyed by an incremented integer"); + Map dropIds = new HashMap<>(); + + long i = 1; + + for (Drop availableDrop : dropClient.getDrops(repositoryId, getImportedFilter(), 0L, numberOfDropsToFetchParam).getContent()) { + dropIds.put(i++, availableDrop); + } + + return dropIds; + } + + /** + * Returns the "imported" filter to be passed to {@link DropClient#getDrops(java.lang.Long, java.lang.Boolean, java.lang.Long, java.lang.Long) + * } based on the CLI parameter {@link #alsoShowImportedParam}. + * + * @return the imported filter to get drops + */ + private Boolean getImportedFilter() { + return alsoShowImportedParam ? null : false; + } + + /** + * Gets the list of selected {@link Drop#id}. + * + *

+ * First, reads a drop number from the console and then gets the + * {@link Drop} from the map of available {@link Drop}s. + * + * @param numberedAvailableDrops candidate {@link Drop}s for selection + * @return selected {@link Drop#id} + * @throws CommandException if the input doesn't match a number from the map + * of available {@link Drop}s + */ + private List getSelectedDropIds(Map numberedAvailableDrops) throws CommandException { + return getFromConsoleDropIds(numberedAvailableDrops); + } + + private List getFromConsoleDropIds(Map numberedAvailableDrops) throws CommandException { + consoleWriter.newLine().a("Enter Drop number to cancel").println(); + Long dropNumber = console.readLine(Long.class); + + if (!numberedAvailableDrops.containsKey(dropNumber)) { + throw new CommandException("Please enter a number from the list: " + numberedAvailableDrops.keySet()); + } + + Long selectId = numberedAvailableDrops.get(dropNumber).getId(); + + return Arrays.asList(selectId); + } +} diff --git a/cli/src/main/java/com/box/l10n/mojito/cli/command/DropCompleteCommand.java b/cli/src/main/java/com/box/l10n/mojito/cli/command/DropCompleteCommand.java new file mode 100644 index 0000000000..9e92c4576b --- /dev/null +++ b/cli/src/main/java/com/box/l10n/mojito/cli/command/DropCompleteCommand.java @@ -0,0 +1,176 @@ +package com.box.l10n.mojito.cli.command; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.box.l10n.mojito.cli.command.param.Param; +import com.box.l10n.mojito.cli.console.Console; +import com.box.l10n.mojito.cli.console.ConsoleWriter; +import com.box.l10n.mojito.rest.client.DropClient; +import com.box.l10n.mojito.rest.entity.Drop; +import com.box.l10n.mojito.rest.entity.Repository; +import org.fusesource.jansi.Ansi.Color; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Command to cancel a drop. Displays the list of drops available and ask the + * user the drop id to be cancelled. + * + * @author jaurambault, wadimw + */ +@Component +@Scope("prototype") +@Parameters(commandNames = {"drop-complete"}, commandDescription = "Force complete a partially imported drop") +public class DropCompleteCommand extends Command { + + /** + * logger + */ + static Logger logger = LoggerFactory.getLogger(DropCompleteCommand.class); + + @Autowired + ConsoleWriter consoleWriter; + + @Parameter(names = {"--drop-id", "-i"}, arity = 1, required = false, description = "ID of a drop to mark as complete (skip drop fetching and process only given ID)") + Long dropId = null; + + @Parameter(names = {Param.REPOSITORY_LONG, Param.REPOSITORY_SHORT}, arity = 1, required = true, description = Param.REPOSITORY_DESCRIPTION) + String repositoryParam; + + @Parameter(names = {"--number-drops-fetched"}, arity = 1, required = false, description = "Number of drops fetched") + Long numberOfDropsToFetchParam = 10L; + + @Parameter(names = {"--show-all", "-all"}, required = false, description = "Show all drops (already imported drops are hidden by default)") + Boolean alsoShowImportedParam = false; + + @Autowired + CommandHelper commandHelper; + + @Autowired + Console console; + + @Autowired + DropClient dropClient; + + @Override + public void execute() throws CommandException { + if (dropId != null) { + completeSpecifiedDrop(); + } else { + completeDropsSelectingFromAvailableList(); + } + consoleWriter.newLine().fg(Color.GREEN).a("Finished").println(2); + } + + private void completeSpecifiedDrop() throws CommandException { + consoleWriter.newLine().a("Complete drop: ").fg(Color.CYAN).a(dropId).println(2); + + dropClient.completeDrop(dropId); + } + + private void completeDropsSelectingFromAvailableList() throws CommandException { + Repository repository = commandHelper.findRepositoryByName(repositoryParam); + Map numberedAvailableDrops = getNumberedAvailableDrops(repository.getId()); + + if (numberedAvailableDrops.isEmpty()) { + consoleWriter.newLine().a("No drop available").println(); + } else { + consoleWriter.newLine().a("Drops available").println(); + + logger.debug("Display drops information"); + for (Map.Entry entry : numberedAvailableDrops.entrySet()) { + + Drop drop = entry.getValue(); + + consoleWriter.a(" ").fg(Color.CYAN).a(entry.getKey()).reset(). + a(" - id: ").fg(Color.MAGENTA).a(drop.getId()).reset(). + a(", name: ").fg(Color.MAGENTA).a(drop.getName()).reset(); + + if (Boolean.TRUE.equals(drop.getCanceled())) { + consoleWriter.fg(Color.GREEN).a(" CANCELED"); + } else if (drop.getLastImportedDate() == null) { + consoleWriter.fg(Color.GREEN).a(" NEW"); + } else { + consoleWriter.a(", last import: ").fg(Color.MAGENTA).a(drop.getLastImportedDate()); + } + + consoleWriter.println(); + } + + List dropIds = getSelectedDropIds(numberedAvailableDrops); + + for(Long dropId: dropIds) { + consoleWriter.newLine().a("Complete drop: ").fg(Color.CYAN).a(dropId).reset().a(" in repository: ").fg(Color.CYAN).a(repositoryParam).println(2); + // TODO make drop complete a pollable task to get come confirmation that the action has been performed + dropClient.completeDrop(dropId); + } + } + } + + /** + * Gets available {@link Drop}s and assign them a number (map key) to be + * referenced in the console input for selection. + * + * @return + */ + private Map getNumberedAvailableDrops(Long repositoryId) { + + logger.debug("Build a map of drops keyed by an incremented integer"); + Map dropIds = new HashMap<>(); + + long i = 1; + + for (Drop availableDrop : dropClient.getDrops(repositoryId, getImportedFilter(), 0L, numberOfDropsToFetchParam).getContent()) { + dropIds.put(i++, availableDrop); + } + + return dropIds; + } + + /** + * Returns the "imported" filter to be passed to {@link DropClient#getDrops(java.lang.Long, java.lang.Boolean, java.lang.Long, java.lang.Long) + * } based on the CLI parameter {@link #alsoShowImportedParam}. + * + * @return the imported filter to get drops + */ + private Boolean getImportedFilter() { + return alsoShowImportedParam ? null : false; + } + + /** + * Gets the list of selected {@link Drop#id}. + * + *

+ * First, reads a drop number from the console and then gets the + * {@link Drop} from the map of available {@link Drop}s. + * + * @param numberedAvailableDrops candidate {@link Drop}s for selection + * @return selected {@link Drop#id} + * @throws CommandException if the input doesn't match a number from the map + * of available {@link Drop}s + */ + private List getSelectedDropIds(Map numberedAvailableDrops) throws CommandException { + return getFromConsoleDropIds(numberedAvailableDrops); + } + + private List getFromConsoleDropIds(Map numberedAvailableDrops) throws CommandException { + consoleWriter.newLine().a("Enter Drop number to force complete").println(); + Long dropNumber = console.readLine(Long.class); + + if (!numberedAvailableDrops.containsKey(dropNumber)) { + throw new CommandException("Please enter a number from the list: " + numberedAvailableDrops.keySet()); + } + + Long selectId = numberedAvailableDrops.get(dropNumber).getId(); + + return Arrays.asList(selectId); + } +} diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/client/DropClient.java b/restclient/src/main/java/com/box/l10n/mojito/rest/client/DropClient.java index 9ca91d609c..7f0ea7e6de 100644 --- a/restclient/src/main/java/com/box/l10n/mojito/rest/client/DropClient.java +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/client/DropClient.java @@ -1,6 +1,7 @@ package com.box.l10n.mojito.rest.client; import com.box.l10n.mojito.rest.entity.Drop; +import com.box.l10n.mojito.rest.entity.CancelDropConfig; import com.box.l10n.mojito.rest.entity.ExportDropConfig; import com.box.l10n.mojito.rest.entity.ImportDropConfig; import com.box.l10n.mojito.rest.entity.ImportXliffBody; @@ -165,4 +166,53 @@ public String importXiff(String xliffContent, Long repositoryId, boolean isTrans ImportXliffBody.class).getXliffContent(); } + /** + * Cancels a drop for a given drop ID + * + * @param dropId the drop ID to be cancelled + * @return {@link CancelDropConfig} that contains information about the drop + * being cancelled + */ + public CancelDropConfig cancelDrop(Long dropId) { + + CancelDropConfig cancelDropConfig = new CancelDropConfig(); + cancelDropConfig.setDropId(dropId); + + String cancelPath = UriComponentsBuilder + .fromPath(getBasePathForEntity()) + .pathSegment("cancel") + .toUriString(); + + return authenticatedRestTemplate.postForObject( + cancelPath, + cancelDropConfig, + CancelDropConfig.class); + } + + /** + * Force completes a drop for a given drop ID + * + * @param dropId the drop ID to be cancelled + */ + public void completeDrop(Long dropId) { + Map params = new HashMap<>(); + params.put("dropId", dropId.toString()); + + String completePath = UriComponentsBuilder + .fromPath(getBasePathForEntity()) + .pathSegment("complete", "{dropId}") + .buildAndExpand(params) + .toUriString(); + + authenticatedRestTemplate.postForObject( + completePath, + null, + Void.class); + + // NOTE: currently there is no response from "complete" endpoint + // so there is no way to tell if the drop has actually been marked as completed; + // in particular, if given drop has NOT ever been partially imported yet, + // the call succeeds but the drop does not seem get marked as completed according to webapp UI + // SEE: webapp/src/main/java/com/box/l10n/mojito/service/drop/DropService.java#completeDrop + } } diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CancelDropConfig.java b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CancelDropConfig.java new file mode 100644 index 0000000000..f538214d72 --- /dev/null +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/entity/CancelDropConfig.java @@ -0,0 +1,42 @@ +package com.box.l10n.mojito.rest.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Configuration to trigger the drop cancellation process + * + * @author aloison, wadimw + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class CancelDropConfig { + @JsonProperty(required = true) + Long dropId; + + PollableTask pollableTask; + + public Long getDropId() { + return dropId; + } + + public void setDropId(Long dropId) { + this.dropId = dropId; + } + + @JsonProperty + public PollableTask getPollableTask() { + return pollableTask; + } + + /** + * @JsonIgnore because this pollableTask is read only data generated by the + * server side, it is not aimed to by external process via WS + * + * @param pollableTask + */ + @JsonIgnore + public void setPollableTask(PollableTask pollableTask) { + this.pollableTask = pollableTask; + } +}