This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 2033c68721b77f5e96ce672377a92aeca9cce9f8 Author: Benoit Tellier <[email protected]> AuthorDate: Thu Dec 12 08:27:46 2019 +0100 JAMES-3006 Add a TaskFactory for modular task registration in webadmin --- .../apache/james/webadmin/tasks/TaskFactory.java | 145 +++++++++++++++++++ .../apache/james/webadmin/tasks/TaskGenerator.java | 54 +++++++ .../james/webadmin/tasks/TaskRegistrationKey.java | 66 +++++++++ .../james/webadmin/tasks/TaskFactoryTest.java | 159 +++++++++++++++++++++ .../webadmin/tasks/TaskRegistrationKeyTest.java | 53 +++++++ 5 files changed, 477 insertions(+) diff --git a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/tasks/TaskFactory.java b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/tasks/TaskFactory.java new file mode 100644 index 0000000..7db9bef --- /dev/null +++ b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/tasks/TaskFactory.java @@ -0,0 +1,145 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.webadmin.tasks; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import org.apache.commons.lang3.StringUtils; +import org.apache.james.task.Task; +import org.apache.james.task.TaskManager; + +import com.github.fge.lambdas.Throwing; +import com.github.steveash.guavate.Guavate; +import com.google.common.base.Joiner; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import spark.Request; +import spark.Route; + +public class TaskFactory implements TaskGenerator { + private static final String DEFAULT_PARAMETER = "action"; + + public static class Builder { + private Optional<String> taskParameterName; + private ImmutableMap.Builder<TaskRegistrationKey, TaskGenerator> tasks; + + public Builder() { + taskParameterName = Optional.empty(); + tasks = ImmutableMap.builder(); + } + + public Builder parameterName(String parameterName) { + this.taskParameterName = Optional.of(parameterName); + return this; + } + + public Builder registrations(TaskRegistration... taskRegistrations) { + this.tasks.putAll(Arrays.stream(taskRegistrations) + .collect(Guavate.toImmutableMap( + TaskRegistration::registrationKey, + Function.identity()))); + return this; + } + + public Builder register(TaskRegistrationKey key, TaskGenerator taskGenerator) { + this.tasks.put(key, taskGenerator); + return this; + } + + public TaskFactory build() { + ImmutableMap<TaskRegistrationKey, TaskGenerator> registrations = tasks.build(); + Preconditions.checkState(!registrations.isEmpty()); + return new TaskFactory( + taskParameterName.orElse(DEFAULT_PARAMETER), + registrations); + } + + public Route buildAsRoute(TaskManager taskManager) { + return build().asRoute(taskManager); + } + } + + public static class TaskRegistration implements TaskGenerator { + private final TaskRegistrationKey taskRegistrationKey; + private final TaskGenerator taskGenerator; + + public TaskRegistration(TaskRegistrationKey taskRegistrationKey, TaskGenerator taskGenerator) { + this.taskRegistrationKey = taskRegistrationKey; + this.taskGenerator = taskGenerator; + } + + public TaskRegistrationKey registrationKey() { + return taskRegistrationKey; + } + + @Override + public Task generate(Request request) throws Exception { + return taskGenerator.generate(request); + } + } + + public static Builder builder() { + return new Builder(); + } + + private final String taskParameterName; + private final Map<TaskRegistrationKey, TaskGenerator> taskGenerators; + + private TaskFactory(String taskParameterName, Map<TaskRegistrationKey, TaskGenerator> taskGenerators) { + this.taskParameterName = taskParameterName; + this.taskGenerators = taskGenerators; + } + + @Override + public Task generate(Request request) throws Exception { + TaskRegistrationKey registrationKey = parseRegistrationKey(request); + return Optional.ofNullable(taskGenerators.get(registrationKey)) + .map(Throwing.<TaskGenerator, Task>function(taskGenerator -> taskGenerator.generate(request)).sneakyThrow()) + .orElseThrow(() -> new IllegalArgumentException("Invalid value supplied for '" + taskParameterName + "': " + registrationKey.asString() + + ". " + supportedValueMessage())); + } + + private TaskRegistrationKey parseRegistrationKey(Request request) { + return Optional.ofNullable(request.queryParams(taskParameterName)) + .map(this::validateParameter) + .map(TaskRegistrationKey::of) + .orElseThrow(() -> new IllegalArgumentException("'" + taskParameterName + "' query parameter is compulsory. " + supportedValueMessage())); + } + + private String validateParameter(String parameter) { + if (StringUtils.isBlank(parameter)) { + throw new IllegalArgumentException("'" + taskParameterName + "' query parameter cannot be empty or blank. " + supportedValueMessage()); + } + return parameter; + } + + private String supportedValueMessage() { + ImmutableList<String> supportedTasks = taskGenerators.keySet() + .stream() + .map(TaskRegistrationKey::asString) + .collect(Guavate.toImmutableList()); + return "Supported values are [" + Joiner.on(", ").join(supportedTasks) + "]"; + } +} diff --git a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/tasks/TaskGenerator.java b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/tasks/TaskGenerator.java new file mode 100644 index 0000000..09d759e --- /dev/null +++ b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/tasks/TaskGenerator.java @@ -0,0 +1,54 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.webadmin.tasks; + +import org.apache.james.task.Task; +import org.apache.james.task.TaskId; +import org.apache.james.task.TaskManager; +import org.apache.james.webadmin.dto.TaskIdDto; + +import spark.Request; +import spark.Response; +import spark.Route; + +public interface TaskGenerator { + class TaskRoute implements Route { + private final TaskGenerator taskGenerator; + private final TaskManager taskManager; + + TaskRoute(TaskGenerator taskGenerator, TaskManager taskManager) { + this.taskGenerator = taskGenerator; + this.taskManager = taskManager; + } + + @Override + public Object handle(Request request, Response response) throws Exception { + Task task = taskGenerator.generate(request); + TaskId taskId = taskManager.submit(task); + return TaskIdDto.respond(response, taskId); + } + } + + Task generate(Request request) throws Exception; + + default Route asRoute(TaskManager taskManager) { + return new TaskRoute(this, taskManager); + } +} diff --git a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/tasks/TaskRegistrationKey.java b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/tasks/TaskRegistrationKey.java new file mode 100644 index 0000000..cf1e55e --- /dev/null +++ b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/tasks/TaskRegistrationKey.java @@ -0,0 +1,66 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.webadmin.tasks; + +import java.util.Objects; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; + +public class TaskRegistrationKey { + public static TaskRegistrationKey of(String value) { + Preconditions.checkNotNull(value, "TaskRegistrationKey cannot be null"); + Preconditions.checkArgument(!value.isEmpty(), "TaskRegistrationKey cannot be empty"); + + return new TaskRegistrationKey(value); + } + + private final String value; + + private TaskRegistrationKey(String value) { + this.value = value; + } + + public String asString() { + return value; + } + + @Override + public final boolean equals(Object o) { + if (o instanceof TaskRegistrationKey) { + TaskRegistrationKey that = (TaskRegistrationKey) o; + + return Objects.equals(this.value, that.value); + } + return false; + } + + @Override + public final int hashCode() { + return Objects.hash(value); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("value", value) + .toString(); + } +} diff --git a/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/tasks/TaskFactoryTest.java b/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/tasks/TaskFactoryTest.java new file mode 100644 index 0000000..648fc84 --- /dev/null +++ b/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/tasks/TaskFactoryTest.java @@ -0,0 +1,159 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.webadmin.tasks; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.james.task.Task; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import spark.Request; + +class TaskFactoryTest { + static final Task TASK_1 = mock(Task.class); + static final Task TASK_2 = mock(Task.class); + static final TaskRegistrationKey KEY_1 = TaskRegistrationKey.of("task1"); + static final TaskRegistrationKey KEY_2 = TaskRegistrationKey.of("task2"); + + Request request; + TaskFactory taskFactory; + TaskFactory singleTaskFactory; + + @BeforeEach + void setUp() { + request = mock(Request.class); + taskFactory = TaskFactory.builder() + .register(KEY_1, any -> TASK_1) + .register(KEY_2, any -> TASK_2) + .build(); + singleTaskFactory = TaskFactory.builder() + .register(KEY_1, any -> TASK_1) + .build(); + } + + @Test + void buildShouldThrowWhenNoTasks() { + assertThatThrownBy(() -> TaskFactory.builder().build()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void generateShouldThrowFormattedMessageWhenNoTaskParamAndSeveralOptions() { + assertThatThrownBy(() -> taskFactory.generate(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("'action' query parameter is compulsory. Supported values are [task1, task2]"); + } + + @Test + void generateShouldThrowFormattedMessageWhenNoTaskParam() { + assertThatThrownBy(() -> singleTaskFactory.generate(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("'action' query parameter is compulsory. Supported values are [task1]"); + } + + @Test + void generateShouldThrowWhenCustomParameterValueIsInvalid() { + TaskFactory taskFactory = TaskFactory.builder() + .parameterName("custom") + .register(KEY_1, any -> TASK_1) + .build(); + + when(request.queryParams("custom")).thenReturn("unknown"); + + assertThatThrownBy(() -> taskFactory.generate(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid value supplied for 'custom': unknown. Supported values are [task1]"); + } + + @Test + void generateShouldThrowWhenCustomParameterNotSpecified() { + TaskFactory taskFactory = TaskFactory.builder() + .parameterName("custom") + .register(KEY_1, any -> TASK_1) + .build(); + + when(request.queryParams("action")).thenReturn("unknown"); + + assertThatThrownBy(() -> taskFactory.generate(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("'custom' query parameter is compulsory. Supported values are [task1]"); + } + + @Test + void generateShouldThrowFormattedMessageWhenUnknownTaskParamAndSeveralOptions() { + when(request.queryParams("action")).thenReturn("unknown"); + + assertThatThrownBy(() -> taskFactory.generate(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid value supplied for 'action': unknown. Supported values are [task1, task2]"); + } + + @Test + void generateShouldThrowFormattedMessageWhenUnknownTaskParam() { + when(request.queryParams("action")).thenReturn("unknown"); + + assertThatThrownBy(() -> singleTaskFactory.generate(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid value supplied for 'action': unknown. Supported values are [task1]"); + } + + @Test + void generateShouldThrowWhenEmptyTaskParam() { + when(request.queryParams("action")).thenReturn(""); + + assertThatThrownBy(() -> singleTaskFactory.generate(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("'action' query parameter cannot be empty or blank. Supported values are [task1]"); + } + + @Test + void generateShouldThrowWhenBlankTaskParam() { + when(request.queryParams("action")).thenReturn(" "); + + assertThatThrownBy(() -> singleTaskFactory.generate(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("'action' query parameter cannot be empty or blank. Supported values are [task1]"); + } + + @Test + void generateShouldCreateCorrespondingTask() throws Exception { + when(request.queryParams("action")).thenReturn("task1"); + + assertThat(singleTaskFactory.generate(request)) + .isSameAs(TASK_1); + } + + @Test + void generateShouldHandleCustomTaskParameter() throws Exception { + TaskFactory taskFactory = TaskFactory.builder() + .parameterName("custom") + .register(KEY_1, any -> TASK_1) + .build(); + + when(request.queryParams("custom")).thenReturn("task1"); + + assertThat(taskFactory.generate(request)) + .isSameAs(TASK_1); + } +} \ No newline at end of file diff --git a/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/tasks/TaskRegistrationKeyTest.java b/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/tasks/TaskRegistrationKeyTest.java new file mode 100644 index 0000000..3d7babe --- /dev/null +++ b/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/tasks/TaskRegistrationKeyTest.java @@ -0,0 +1,53 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.webadmin.tasks; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; + +class TaskRegistrationKeyTest { + @Test + void shouldMatchBeanContract() { + EqualsVerifier.forClass(TaskRegistrationKey.class) + .verify(); + } + + @Test + void ofShouldThrowWhenNull() { + assertThatThrownBy(() -> TaskRegistrationKey.of(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + void ofShouldThrowWhenEmpty() { + assertThatThrownBy(() -> TaskRegistrationKey.of("")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void asStringShouldReturnRawValue() { + assertThat(TaskRegistrationKey.of("reindex").asString()) + .isEqualTo("reindex"); + } +} \ No newline at end of file --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
