Skip to content

Commit

Permalink
JAMES-2663 Adding action parameter to restoring API path
Browse files Browse the repository at this point in the history
  • Loading branch information
trantienduchn authored and chibenwa committed Mar 12, 2019
1 parent daa40dc commit 8ccfd9c
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 23 deletions.
Expand Up @@ -21,22 +21,31 @@


import static org.apache.james.webadmin.Constants.SEPARATOR; import static org.apache.james.webadmin.Constants.SEPARATOR;


import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

import javax.inject.Inject; import javax.inject.Inject;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;


import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.StringUtils;
import org.apache.james.core.User; import org.apache.james.core.User;
import org.apache.james.task.Task;
import org.apache.james.task.TaskId; import org.apache.james.task.TaskId;
import org.apache.james.task.TaskManager; import org.apache.james.task.TaskManager;
import org.apache.james.webadmin.Constants; import org.apache.james.webadmin.Constants;
import org.apache.james.webadmin.Routes; import org.apache.james.webadmin.Routes;
import org.apache.james.webadmin.dto.TaskIdDto; import org.apache.james.webadmin.dto.TaskIdDto;
import org.apache.james.webadmin.utils.ErrorResponder;
import org.apache.james.webadmin.utils.JsonTransformer; import org.apache.james.webadmin.utils.JsonTransformer;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;


import com.github.steveash.guavate.Guavate;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;


import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParam;
Expand All @@ -53,9 +62,39 @@
@Produces(Constants.JSON_CONTENT_TYPE) @Produces(Constants.JSON_CONTENT_TYPE)
public class DeletedMessagesVaultRoutes implements Routes { public class DeletedMessagesVaultRoutes implements Routes {


enum UserVaultAction {
RESTORE("restore");

static Optional<UserVaultAction> getAction(String value) {
Preconditions.checkNotNull(value, "action cannot be null");
Preconditions.checkArgument(StringUtils.isNotBlank(value), "action cannot be empty or blank");

return Stream.of(values())
.filter(action -> action.value.equals(value))
.findFirst();
}

private static List<String> plainValues() {
return Stream.of(values())
.map(UserVaultAction::getValue)
.collect(Guavate.toImmutableList());
}

private final String value;

UserVaultAction(String value) {
this.value = value;
}

public String getValue() {
return value;
}
}

public static final String ROOT_PATH = "deletedMessages/user"; public static final String ROOT_PATH = "deletedMessages/user";
private static final String USER_PATH_PARAM = "user"; private static final String USER_PATH_PARAM = "user";
private static final String RESTORE_PATH = ROOT_PATH + SEPARATOR + ":" + USER_PATH_PARAM; private static final String RESTORE_PATH = ROOT_PATH + SEPARATOR + ":" + USER_PATH_PARAM;
private static final String ACTION_QUERY_PARAM = "action";


private final RestoreService vaultRestore; private final RestoreService vaultRestore;
private final JsonTransformer jsonTransformer; private final JsonTransformer jsonTransformer;
Expand All @@ -77,7 +116,7 @@ public String getBasePath() {


@Override @Override
public void define(Service service) { public void define(Service service) {
service.post(RESTORE_PATH, this::restore, jsonTransformer); service.post(RESTORE_PATH, this::userActions, jsonTransformer);
} }


@POST @POST
Expand All @@ -91,28 +130,51 @@ public void define(Service service) {
dataType = "String", dataType = "String",
defaultValue = "none", defaultValue = "none",
example = "user@james.org", example = "user@james.org",
value = "Compulsory. Needs to be a valid username represent for an user had requested to restore deleted emails") value = "Compulsory. Needs to be a valid username represent for an user had requested to restore deleted emails"),
@ApiImplicitParam(
required = true,
dataType = "String",
name = "action",
paramType = "query",
example = "?action=restore",
value = "Compulsory. Needs to be a valid action represent for an operation to perform on the Deleted Message Vault, " +
"valid action should be in the list (restore)")
}) })
@ApiResponses(value = { @ApiResponses(value = {
@ApiResponse(code = HttpStatus.CREATED_201, message = "Task is created", response = TaskIdDto.class), @ApiResponse(code = HttpStatus.CREATED_201, message = "Task is created", response = TaskIdDto.class),
@ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "Bad request - user param is invalid"), @ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "Bad request - user param is invalid"),
@ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side.") @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side.")
}) })
private TaskIdDto restore(Request request, Response response) { private TaskIdDto userActions(Request request, Response response) {
User userToRestore = extractUser(request); UserVaultAction requestedAction = extractUserVaultAction(request);
TaskId taskId = taskManager.submit(new DeletedMessagesVaultRestoreTask(userToRestore, vaultRestore));
Task requestedTask = generateTask(requestedAction, request);
TaskId taskId = taskManager.submit(requestedTask);
return TaskIdDto.respond(response, taskId); return TaskIdDto.respond(response, taskId);
} }


private User extractUser(Request request) { private Task generateTask(UserVaultAction requestedAction, Request request) {
try { User userToRestore = User.fromUsername(request.params(USER_PATH_PARAM));
return User.fromUsername(request.params(USER_PATH_PARAM));
} catch (IllegalArgumentException e) { switch (requestedAction) {
throw ErrorResponder.builder() case RESTORE:
.statusCode(HttpStatus.BAD_REQUEST_400) return new DeletedMessagesVaultRestoreTask(userToRestore, vaultRestore);
.type(ErrorResponder.ErrorType.INVALID_ARGUMENT) default:
.message(e.getMessage()) throw new NotImplementedException(requestedAction + " is not yet handled.");
.haltError();
} }
} }

private UserVaultAction extractUserVaultAction(Request request) {
String actionParam = request.queryParams(ACTION_QUERY_PARAM);
return Optional.ofNullable(actionParam)
.map(this::getUserVaultAction)
.orElseThrow(() -> new IllegalArgumentException("action parameter is missing"));
}

private UserVaultAction getUserVaultAction(String actionString) {
return UserVaultAction.getAction(actionString)
.orElseThrow(() -> new IllegalArgumentException(String.format("'%s' is not a valid action. Supported values are: (%s)",
actionString,
Joiner.on(",").join(UserVaultAction.plainValues()))));
}
} }
Expand Up @@ -116,19 +116,78 @@ void afterEach() {
class ValidationTest { class ValidationTest {


@Test @Test
void restoreShouldReturnInvalidWhenUserIsInvalid() { void restoreShouldReturnInvalidWhenActionIsMissing() {
when() when()
.post(USER.asString())
.then()
.statusCode(HttpStatus.BAD_REQUEST_400)
.body("statusCode", is(400))
.body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
.body("message", is(notNullValue()))
.body("details", is(notNullValue()));
}

@Test
void restoreShouldReturnInvalidWhenPassingEmptyAction() {
given()
.queryParam("action", "")
.when()
.post(USER.asString())
.then()
.statusCode(HttpStatus.BAD_REQUEST_400)
.body("statusCode", is(400))
.body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
.body("message", is(notNullValue()))
.body("details", is(notNullValue()));
}

@Test
void restoreShouldReturnInvalidWhenActionIsInValid() {
given()
.queryParam("action", "invalid action")
.when()
.post(USER.asString())
.then()
.statusCode(HttpStatus.BAD_REQUEST_400)
.body("statusCode", is(400))
.body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
.body("message", is(notNullValue()))
.body("details", is(notNullValue()));
}

@Test
void restoreShouldReturnInvalidWhenPassingCaseInsensitiveAction() {
given()
.queryParam("action", "RESTORE")
.when()
.post(USER.asString())
.then()
.statusCode(HttpStatus.BAD_REQUEST_400)
.body("statusCode", is(400))
.body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
.body("message", is(notNullValue()))
.body("details", is(notNullValue()));
}

@Test
void restoreShouldReturnInvalidWhenUserIsInvalid() {
given()
.queryParam("action", "restore")
.when()
.post("not@valid@user.com") .post("not@valid@user.com")
.then() .then()
.statusCode(HttpStatus.BAD_REQUEST_400) .statusCode(HttpStatus.BAD_REQUEST_400)
.body("statusCode", is(400)) .body("statusCode", is(400))
.body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType())) .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
.body("message", is("The username should not contain multiple domain delimiter.")); .body("message", is(notNullValue()))
.body("details", is(notNullValue()));
} }


@Test @Test
void postShouldReturnNotFoundWhenNoUserPathParameter() { void postShouldReturnNotFoundWhenNoUserPathParameter() {
when() given()
.queryParam("action", "restore")
.when()
.post() .post()
.then() .then()
.statusCode(HttpStatus.NOT_FOUND_404) .statusCode(HttpStatus.NOT_FOUND_404)
Expand All @@ -151,11 +210,13 @@ void restoreShouldProduceFailedTaskWhenTheVaultGetsError() {
.search(any(), any()); .search(any(), any());


String taskId = with() String taskId = with()
.queryParam("action", "restore")
.post(USER.asString()) .post(USER.asString())
.jsonPath() .jsonPath()
.get("taskId"); .get("taskId");


given() given()
.queryParam("action", "restore")
.basePath(TasksRoutes.BASE) .basePath(TasksRoutes.BASE)
.when() .when()
.get(taskId + "/await") .get(taskId + "/await")
Expand Down Expand Up @@ -185,6 +246,7 @@ void restoreShouldProduceFailedTaskWithErrorRestoreCountWhenMessageAppendGetsErr
.appendMessage(any(), any()); .appendMessage(any(), any());


String taskId = with() String taskId = with()
.queryParam("action", "restore")
.post(USER.asString()) .post(USER.asString())
.jsonPath() .jsonPath()
.get("taskId"); .get("taskId");
Expand Down Expand Up @@ -214,6 +276,7 @@ void restoreShouldProduceFailedTaskWhenMailboxMangerGetsError() throws Exception
.createMailbox(any(MailboxPath.class), any(MailboxSession.class)); .createMailbox(any(MailboxPath.class), any(MailboxSession.class));


String taskId = with() String taskId = with()
.queryParam("action", "restore")
.post(USER.asString()) .post(USER.asString())
.jsonPath() .jsonPath()
.get("taskId"); .get("taskId");
Expand All @@ -236,7 +299,9 @@ void restoreShouldProduceFailedTaskWhenMailboxMangerGetsError() throws Exception


@Test @Test
void restoreShouldReturnATaskCreated() { void restoreShouldReturnATaskCreated() {
when() given()
.queryParam("action", "restore")
.when()
.post(USER.asString()) .post(USER.asString())
.then() .then()
.statusCode(HttpStatus.CREATED_201) .statusCode(HttpStatus.CREATED_201)
Expand All @@ -249,6 +314,7 @@ void restoreShouldProduceASuccessfulTaskWithAdditionalInformation() {
vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block(); vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block();


String taskId = with() String taskId = with()
.queryParam("action", "restore")
.post(USER.asString()) .post(USER.asString())
.jsonPath() .jsonPath()
.get("taskId"); .get("taskId");
Expand All @@ -275,6 +341,7 @@ void restoreShouldKeepAllMessagesInTheVaultOfCorrespondingUser() {
vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block(); vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block();


String taskId = with() String taskId = with()
.queryParam("action", "restore")
.post(USER.asString()) .post(USER.asString())
.jsonPath() .jsonPath()
.get("taskId"); .get("taskId");
Expand Down Expand Up @@ -304,6 +371,7 @@ void restoreShouldNotDeleteExistingMessagesInTheUserMailbox() throws Exception {
vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block(); vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block();


String taskId = with() String taskId = with()
.queryParam("action", "restore")
.post(USER.asString()) .post(USER.asString())
.jsonPath() .jsonPath()
.get("taskId"); .get("taskId");
Expand All @@ -325,6 +393,7 @@ void restoreShouldAppendAllMessageFromVaultToRestoreMailboxOfCorrespondingUser()
vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block(); vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block();


String taskId = with() String taskId = with()
.queryParam("action", "restore")
.post(USER.asString()) .post(USER.asString())
.jsonPath() .jsonPath()
.get("taskId"); .get("taskId");
Expand All @@ -348,6 +417,7 @@ void restoreShouldNotAppendMessagesToAnOtherUserMailbox() throws Exception {
vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block(); vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block();


String taskId = with() String taskId = with()
.queryParam("action", "restore")
.post(USER.asString()) .post(USER.asString())
.jsonPath() .jsonPath()
.get("taskId"); .get("taskId");
Expand Down
13 changes: 9 additions & 4 deletions src/site/markdown/server/manage-webadmin.md
Expand Up @@ -2560,17 +2560,22 @@ Here are the following actions available on the 'Deleted Messages Vault'
Deleted messages of a specific user can be restored by calling the following endpoint: Deleted messages of a specific user can be restored by calling the following endpoint:


``` ```
curl -XPOST http://ip:port/deletedMessages/user/userToRestore@domain.ext curl -XPOST http://ip:port/deletedMessages/user/userToRestore@domain.ext?action=restore
``` ```


**All** messages in the Deleted Messages Vault of an specified user will be appended to his 'Restored-Messages' mailbox, which will be created if needed. **All** messages in the Deleted Messages Vault of an specified user will be appended to his 'Restored-Messages' mailbox, which will be created if needed.


**Note**: Restoring matched messages by queries is not supported yet **Note**:

- Restoring matched messages by queries is not supported yet
- Query parameter `action` is required and should have value `restore` to represent for restoring feature. Otherwise, a bad request response will be returned
- Query parameter `action` is case sensitive
Response code: Response code:


- 201: Task for restoring deleted has been created - 201: Task for restoring deleted has been created
- 400: Bad request, user parameter is invalid - 400: Bad request:
- action query param is not present
- action query param is not a valid action
- user parameter is invalid


The scheduled task will have the following type `deletedMessages/restore` and the following `additionalInformation`: The scheduled task will have the following type `deletedMessages/restore` and the following `additionalInformation`:


Expand Down

0 comments on commit 8ccfd9c

Please sign in to comment.