diff options
author | Janos Gyerik <janos.gyerik@sonarsource.com> | 2019-01-17 07:32:31 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2019-02-11 09:11:46 +0100 |
commit | dced64bda1caa518354c12c05a70c167ad280a15 (patch) | |
tree | 383988f1c17c4acab2d547ad95bb3bcf0e409eb0 | |
parent | 96e54a89a4cfa87e304d0978d3a829b30deb99ed (diff) | |
download | sonarqube-dced64bda1caa518354c12c05a70c167ad280a15.tar.gz sonarqube-dced64bda1caa518354c12c05a70c167ad280a15.zip |
SONAR-11626 Add new WS project_analyses/set_baseline
5 files changed, 507 insertions, 2 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ProjectAnalysisModule.java b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ProjectAnalysisModule.java index 158013a5e0f..c5cd2282b3c 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ProjectAnalysisModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ProjectAnalysisModule.java @@ -25,6 +25,7 @@ import org.sonar.server.projectanalysis.ws.DeleteAction; import org.sonar.server.projectanalysis.ws.DeleteEventAction; import org.sonar.server.projectanalysis.ws.ProjectAnalysesWs; import org.sonar.server.projectanalysis.ws.SearchAction; +import org.sonar.server.projectanalysis.ws.SetBaselineAction; import org.sonar.server.projectanalysis.ws.UpdateEventAction; public class ProjectAnalysisModule extends Module { @@ -38,7 +39,8 @@ public class ProjectAnalysisModule extends Module { UpdateEventAction.class, DeleteEventAction.class, DeleteAction.class, - SearchAction.class); + SearchAction.class, + SetBaselineAction.class); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/SetBaselineAction.java b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/SetBaselineAction.java new file mode 100644 index 00000000000..7694ae9b9a1 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/SetBaselineAction.java @@ -0,0 +1,148 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.projectanalysis.ws; + +import com.google.protobuf.Empty; +import javax.annotation.Nullable; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.web.UserRole; +import org.sonar.core.util.Uuids; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.BranchDto; +import org.sonar.db.component.BranchType; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.SnapshotDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.user.UserSession; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.String.format; +import static org.sonar.server.component.ComponentFinder.ParamNames.PROJECT_ID_AND_KEY; +import static org.sonar.server.projectanalysis.ws.ProjectAnalysesWsParameters.PARAM_ANALYSIS; +import static org.sonar.server.projectanalysis.ws.ProjectAnalysesWsParameters.PARAM_BRANCH; +import static org.sonar.server.projectanalysis.ws.ProjectAnalysesWsParameters.PARAM_PROJECT; +import static org.sonar.server.ws.WsUtils.writeProtobuf; + +public class SetBaselineAction implements ProjectAnalysesWsAction { + private final DbClient dbClient; + private final UserSession userSession; + private final ComponentFinder componentFinder; + + public SetBaselineAction(DbClient dbClient, UserSession userSession, ComponentFinder componentFinder) { + this.dbClient = dbClient; + this.userSession = userSession; + this.componentFinder = componentFinder; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("set_baseline") + .setDescription("Set an analysis as the baseline of the New Code Period on a project or a long-lived branch.<br/>" + + "This manually set baseline overrides the `sonar.leak.period` setting.<br/>" + + "Requires one of the following permissions:" + + "<ul>" + + " <li>'Administer System'</li>" + + " <li>'Administer' rights on the specified project</li>" + + "</ul>") + .setSince("7.7") + .setPost(true) + .setHandler(this); + + action.createParam(PARAM_PROJECT) + .setDescription("Project key") + .setRequired(true); + + action.createParam(PARAM_BRANCH) + .setDescription("Branch key"); + + action.createParam(PARAM_ANALYSIS) + .setDescription("Analysis key") + .setExampleValue(Uuids.UUID_EXAMPLE_01) + .setRequired(true); + } + + @Override + public void handle(Request httpRequest, Response httpResponse) throws Exception { + doHandle(httpRequest); + + writeProtobuf(Empty.newBuilder().build(), httpRequest, httpResponse); + } + + private void doHandle(Request request) { + String projectKey = mandatoryNonEmptyParam(request, PARAM_PROJECT); + String branchKey = request.param(PARAM_BRANCH); + checkArgument(branchKey == null || !branchKey.isEmpty(), "The '%s' parameter must not be empty", PARAM_BRANCH); + String analysisUuid = mandatoryNonEmptyParam(request, PARAM_ANALYSIS); + + try (DbSession dbSession = dbClient.openSession(false)) { + ComponentDto projectBranch = getProjectBranch(dbSession, projectKey, branchKey); + SnapshotDto analysis = getAnalysis(dbSession, analysisUuid); + checkRequest(dbSession, projectBranch, branchKey, analysis); + dbClient.branchDao().updateManualBaseline(dbSession, projectBranch.uuid(), analysis.getUuid()); + dbSession.commit(); + } + } + + private static String mandatoryNonEmptyParam(Request request, String param) { + String value = request.mandatoryParam(param); + checkArgument(!value.isEmpty(), "The '%s' parameter must not be empty", param); + return value; + } + + private ComponentDto getProjectBranch(DbSession dbSession, String projectKey, @Nullable String branchKey) { + if (branchKey == null) { + return componentFinder.getByUuidOrKey(dbSession, null, projectKey, PROJECT_ID_AND_KEY); + } + ComponentDto project = componentFinder.getByKeyAndBranch(dbSession, projectKey, branchKey); + + BranchDto branchDto = dbClient.branchDao().selectByUuid(dbSession, project.uuid()) + .orElseThrow(() -> new NotFoundException(format("Branch '%s' is not found", branchKey))); + + checkArgument(branchDto.getBranchType() == BranchType.LONG, + "Not a long-living branch: '%s'", branchKey); + + return project; + } + + private SnapshotDto getAnalysis(DbSession dbSession, String analysisUuid) { + return dbClient.snapshotDao().selectByUuid(dbSession, analysisUuid) + .orElseThrow(() -> new NotFoundException(format("Analysis '%s' is not found", analysisUuid))); + } + + private void checkRequest(DbSession dbSession, ComponentDto projectBranch, @Nullable String branchKey, SnapshotDto analysis) { + userSession.checkComponentPermission(UserRole.ADMIN, projectBranch); + ComponentDto project = dbClient.componentDao().selectByUuid(dbSession, analysis.getComponentUuid()).orElse(null); + + boolean analysisMatchesProjectBranch = project != null && projectBranch.uuid().equals(project.uuid()); + if (branchKey != null) { + checkArgument (analysisMatchesProjectBranch, + "Analysis '%s' does not belong to branch '%s' of project '%s'", + analysis.getUuid(), branchKey, projectBranch.getKey()); + } else { + checkArgument (analysisMatchesProjectBranch, + "Analysis '%s' does not belong to project '%s'", + analysis.getUuid(), projectBranch.getKey()); + } + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ProjectAnalysisModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ProjectAnalysisModuleTest.java index beee3fa54cb..c4b740dc9f9 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ProjectAnalysisModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ProjectAnalysisModuleTest.java @@ -30,6 +30,6 @@ public class ProjectAnalysisModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new ProjectAnalysisModule().configure(container); - assertThat(container.size()).isEqualTo(2 + 6); + assertThat(container.size()).isEqualTo(2 + 7); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/SetBaselineActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/SetBaselineActionTest.java new file mode 100644 index 00000000000..6ea8758481c --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/SetBaselineActionTest.java @@ -0,0 +1,307 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.projectanalysis.ws; + +import com.google.common.collect.ImmutableMap; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.utils.System2; +import org.sonar.api.web.UserRole; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.component.BranchDto; +import org.sonar.db.component.BranchType; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTesting; +import org.sonar.db.component.SnapshotDto; +import org.sonar.server.component.TestComponentFinder; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.WsActionTester; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.server.projectanalysis.ws.ProjectAnalysesWsParameters.PARAM_ANALYSIS; +import static org.sonar.server.projectanalysis.ws.ProjectAnalysesWsParameters.PARAM_BRANCH; +import static org.sonar.server.projectanalysis.ws.ProjectAnalysesWsParameters.PARAM_PROJECT; +import static org.sonar.test.Matchers.regexMatcher; +import static org.sonarqube.ws.client.WsRequest.Method.POST; + +@RunWith(DataProviderRunner.class) +public class SetBaselineActionTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + + @Rule + public DbTester db = DbTester.create(System2.INSTANCE); + private DbClient dbClient = db.getDbClient(); + private DbSession dbSession = db.getSession(); + + private WsActionTester ws = new WsActionTester(new SetBaselineAction(dbClient, userSession, TestComponentFinder.from(db))); + + @Test + public void set_baseline_on_main_branch() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(db.organizations().insert()); + BranchDto branch = new BranchDto() + .setBranchType(BranchType.LONG) + .setProjectUuid(project.uuid()) + .setUuid(project.uuid()) + .setKey("master"); + db.components().insertComponent(project); + db.getDbClient().branchDao().insert(dbSession, branch); + SnapshotDto analysis = db.components().insertSnapshot(project); + logInAsProjectAdministrator(project); + + call(ImmutableMap.of(PARAM_PROJECT, project.getKey(), PARAM_ANALYSIS, analysis.getUuid())); + + BranchDto loaded = dbClient.branchDao().selectByUuid(dbSession, branch.getUuid()).get(); + assertThat(loaded.getManualBaseline()).isEqualTo(analysis.getUuid()); + } + + @Test + public void set_baseline_on_long_living_branch() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(db.organizations().insert()); + BranchDto branch = ComponentTesting.newBranchDto(project.projectUuid(), BranchType.LONG); + db.components().insertProjectBranch(project, branch); + ComponentDto branchComponentDto = ComponentTesting.newProjectBranch(project, branch); + SnapshotDto analysis = db.components().insertSnapshot(branchComponentDto); + logInAsProjectAdministrator(project); + + call(project.getKey(), branch.getKey(), analysis.getUuid()); + + BranchDto loaded = dbClient.branchDao().selectByUuid(dbSession, branch.getUuid()).get(); + assertThat(loaded.getManualBaseline()).isEqualTo(analysis.getUuid()); + } + + @Test + public void fail_when_user_is_not_admin() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(db.organizations().insert()); + BranchDto branch = ComponentTesting.newBranchDto(project.projectUuid(), BranchType.LONG); + db.components().insertProjectBranch(project, branch); + ComponentDto branchComponentDto = ComponentTesting.newProjectBranch(project, branch); + SnapshotDto analysis = db.components().insertSnapshot(branchComponentDto); + + expectedException.expect(ForbiddenException.class); + expectedException.expectMessage("Insufficient privileges"); + + call(project.getKey(), branch.getKey(), analysis.getUuid()); + } + + @Test + @UseDataProvider("missingOrEmptyParamsAndFailureMessage") + public void fail_with_IAE_when_required_param_missing_or_empty(Map<String, String> params, String message) { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage(message); + + call(params); + } + + @DataProvider + public static Object[][] missingOrEmptyParamsAndFailureMessage() { + MapBuilder builder = new MapBuilder() + .put(PARAM_PROJECT, "project key") + .put(PARAM_BRANCH, "branch key") + .put(PARAM_ANALYSIS, "analysis uuid"); + + return new Object[][] { + {builder.put(PARAM_PROJECT, null).map, "The 'project' parameter is missing"}, + {builder.put(PARAM_PROJECT, "").map, "The 'project' parameter must not be empty"}, + {builder.put(PARAM_BRANCH, "").map, "The 'branch' parameter must not be empty"}, + {builder.put(PARAM_ANALYSIS, null).map, "The 'analysis' parameter is missing"}, + {builder.put(PARAM_ANALYSIS, "").map, "The 'analysis' parameter must not be empty"}, + }; + } + + @Test + @UseDataProvider("nonexistentParamsAndFailureMessage") + public void fail_with_IAE_when_required_param_nonexistent(Map<String, String> nonexistentParams, String regex) { + ComponentDto project = ComponentTesting.newPrivateProjectDto(db.organizations().insert()); + BranchDto branch = ComponentTesting.newBranchDto(project.projectUuid(), BranchType.LONG); + db.components().insertProjectBranch(project, branch); + ComponentDto branchComponentDto = ComponentTesting.newProjectBranch(project, branch); + SnapshotDto analysis = db.components().insertSnapshot(branchComponentDto); + logInAsProjectAdministrator(project); + + Map<String, String> params = new HashMap<>(); + params.put(PARAM_PROJECT, project.getKey()); + params.put(PARAM_BRANCH, branch.getKey()); + params.put(PARAM_ANALYSIS, analysis.getUuid()); + params.putAll(nonexistentParams); + + expectedException.expect(NotFoundException.class); + expectedException.expectMessage(regexMatcher(regex)); + + call(params); + } + + @DataProvider + public static Object[][] nonexistentParamsAndFailureMessage() { + MapBuilder builder = new MapBuilder(); + + return new Object[][] { + {builder.put(PARAM_PROJECT, "nonexistent").map, "Component 'nonexistent' on branch .* not found"}, + {builder.put(PARAM_BRANCH, "nonexistent").map, "Component .* on branch 'nonexistent' not found"}, + {builder.put(PARAM_ANALYSIS, "nonexistent").map, "Analysis 'nonexistent' is not found"}, + }; + } + + @Test + public void fail_when_branch_does_not_belong_to_project() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(db.organizations().insert()); + BranchDto branch = ComponentTesting.newBranchDto(project.projectUuid(), BranchType.LONG); + db.components().insertProjectBranch(project, branch); + ComponentDto branchComponentDto = ComponentTesting.newProjectBranch(project, branch); + SnapshotDto analysis = db.components().insertSnapshot(branchComponentDto); + logInAsProjectAdministrator(project); + + ComponentDto otherProject = ComponentTesting.newPrivateProjectDto(db.organizations().insert()); + BranchDto otherBranch = ComponentTesting.newBranchDto(otherProject.projectUuid(), BranchType.LONG); + db.components().insertProjectBranch(otherProject, otherBranch); + ComponentTesting.newProjectBranch(otherProject, otherBranch); + + expectedException.expect(NotFoundException.class); + expectedException.expectMessage(String.format("Component '%s' on branch '%s' not found", project.getKey(), otherBranch.getKey())); + + call(project.getKey(), otherBranch.getKey(), analysis.getUuid()); + } + + @Test + public void fail_when_analysis_does_not_belong_to_main_branch_of_project() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(db.organizations().insert()); + BranchDto branch = new BranchDto() + .setBranchType(BranchType.LONG) + .setProjectUuid(project.uuid()) + .setUuid(project.uuid()) + .setKey("master"); + db.components().insertComponent(project); + db.getDbClient().branchDao().insert(dbSession, branch); + logInAsProjectAdministrator(project); + + ComponentDto otherProject = ComponentTesting.newPrivateProjectDto(db.organizations().insert()); + SnapshotDto otherAnalysis = db.components().insertSnapshot(otherProject); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage(String.format("Analysis '%s' does not belong to project '%s'", + otherAnalysis.getUuid(), project.getKey())); + + call(ImmutableMap.of(PARAM_PROJECT, project.getKey(), PARAM_ANALYSIS, otherAnalysis.getUuid())); + } + + @Test + public void fail_when_analysis_does_not_belong_to_non_main_branch_of_project() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(db.organizations().insert()); + BranchDto branch = ComponentTesting.newBranchDto(project.projectUuid(), BranchType.LONG); + db.components().insertProjectBranch(project, branch); + logInAsProjectAdministrator(project); + + ComponentDto otherProject = ComponentTesting.newPrivateProjectDto(db.organizations().insert()); + SnapshotDto otherAnalysis = db.components().insertProjectAndSnapshot(otherProject); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage(String.format("Analysis '%s' does not belong to branch '%s' of project '%s'", + otherAnalysis.getUuid(), branch.getKey(), project.getKey())); + + call(project.getKey(), branch.getKey(), otherAnalysis.getUuid()); + } + + @Test + public void fail_when_branch_is_not_long() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(db.organizations().insert()); + BranchDto branch = ComponentTesting.newBranchDto(project.projectUuid(), BranchType.SHORT); + db.components().insertProjectBranch(project, branch); + ComponentDto branchComponentDto = ComponentTesting.newProjectBranch(project, branch); + SnapshotDto analysis = db.components().insertSnapshot(branchComponentDto); + logInAsProjectAdministrator(project); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage(String.format("Not a long-living branch: '%s'", branch.getKey())); + + call(project.getKey(), branch.getKey(), analysis.getUuid()); + } + + @Test + public void ws_parameters() { + WebService.Action definition = ws.getDef(); + + assertThat(definition.isPost()).isTrue(); + assertThat(definition.key()).isEqualTo("set_baseline"); + assertThat(definition.since()).isEqualTo("7.7"); + assertThat(definition.isInternal()).isFalse(); + } + + private void logInAsProjectAdministrator(ComponentDto project) { + userSession.logIn().addProjectPermission(UserRole.ADMIN, project); + } + + private void call(Map<String, String> params) { + TestRequest httpRequest = ws.newRequest().setMethod(POST.name()); + + for (Map.Entry<String, String> param : params.entrySet()) { + httpRequest.setParam(param.getKey(), param.getValue()); + } + + httpRequest.execute(); + } + + private void call(String projectKey, String branchKey, String analysisUuid) { + call(ImmutableMap.of( + PARAM_PROJECT, projectKey, + PARAM_BRANCH, branchKey, + PARAM_ANALYSIS, analysisUuid)); + } + + private static class MapBuilder { + private final Map<String, String> map; + + private MapBuilder() { + this.map = Collections.emptyMap(); + } + + private MapBuilder(Map<String, String> map) { + this.map = map; + } + + public MapBuilder put(String key, @Nullable String value) { + Map<String, String> copy = new HashMap<>(map); + if (value == null) { + copy.remove(key); + } else { + copy.put(key, value); + } + return new MapBuilder(copy); + } + } +} diff --git a/sonar-testing-harness/src/main/java/org/sonar/test/Matchers.java b/sonar-testing-harness/src/main/java/org/sonar/test/Matchers.java new file mode 100644 index 00000000000..216683f8273 --- /dev/null +++ b/sonar-testing-harness/src/main/java/org/sonar/test/Matchers.java @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.test; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +/** + * Utility class to provide various Matchers to use in ExpectedException.expectMessage. + */ +public class Matchers { + + private Matchers() { + // utility class, forbidden constructor + } + + public static Matcher<String> regexMatcher(String regex) { + return new TypeSafeMatcher<String>() { + @Override + protected boolean matchesSafely(String item) { + return item.matches(regex); + } + + @Override + public void describeTo(Description description) { + description.appendText("matching regex ").appendValue(regex); + } + }; + } +} |