From 771476ef670cda57dfb089552542819aee4d8fec Mon Sep 17 00:00:00 2001 From: Mark Czotter Date: Mon, 11 Apr 2022 15:57:46 +0200 Subject: [PATCH 1/5] chore(api): improve error message when using API that does not support.. ...reflective access --- .../snowowl/core/context/TerminologyResourceRequest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/context/TerminologyResourceRequest.java b/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/context/TerminologyResourceRequest.java index 5551d842d07..ddc1fdd701e 100644 --- a/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/context/TerminologyResourceRequest.java +++ b/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/context/TerminologyResourceRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 B2i Healthcare Pte Ltd, http://b2i.sg + * Copyright 2021-2022 B2i Healthcare Pte Ltd, http://b2i.sg * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,7 +53,8 @@ public TerminologyResourceRequest(final String toolingId, final String resourceP this.toolingId = toolingId; this.resourcePath = resourcePath; if (resourcePath.startsWith(Branch.MAIN_PATH) && Strings.isNullOrEmpty(toolingId)) { - throw new BadRequestException("Reflective access ('repositoryId/path') to terminology resource content is not supported in this request builder."); + throw new BadRequestException("Reflective access ('repositoryId/path') to terminology resource content is not supported in this API.") + .withDeveloperMessage("No toolingId is specified on API level to ensure the correct reflective access to underlying terminology."); } } From f2727527661a8c4fb014413075015503f5f20772 Mon Sep 17 00:00:00 2001 From: Mark Czotter Date: Wed, 13 Apr 2022 17:39:00 +0200 Subject: [PATCH 2/5] fix(core): ensure retired resources cannot be versioned... ...and their content cannot be changed. Metadata is still allowed to be edited. --- .../rest/codesystem/CodeSystemApiTest.java | 38 +++++++++++-- .../snowowl/core/Resource.java | 3 + .../DefaultTerminologyResourceContext.java | 4 +- ...rminologyResourceCommitRequestBuilder.java | 14 ++++- ...TerminologyResourceStatusCheckRequest.java | 57 +++++++++++++++++++ .../request/version/VersionCreateRequest.java | 4 ++ 6 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/request/TerminologyResourceStatusCheckRequest.java diff --git a/core/com.b2international.snowowl.core.rest.tests/src/com/b2international/snowowl/core/rest/codesystem/CodeSystemApiTest.java b/core/com.b2international.snowowl.core.rest.tests/src/com/b2international/snowowl/core/rest/codesystem/CodeSystemApiTest.java index bf4aea996f7..27de8cb73ed 100644 --- a/core/com.b2international.snowowl.core.rest.tests/src/com/b2international/snowowl/core/rest/codesystem/CodeSystemApiTest.java +++ b/core/com.b2international.snowowl.core.rest.tests/src/com/b2international/snowowl/core/rest/codesystem/CodeSystemApiTest.java @@ -25,8 +25,7 @@ import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.iterableWithSize; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; import java.time.LocalDate; import java.util.List; @@ -40,6 +39,7 @@ import com.b2international.commons.exceptions.NotFoundException; import com.b2international.commons.http.ExtendedLocale; import com.b2international.commons.json.Json; +import com.b2international.snowowl.core.Resource; import com.b2international.snowowl.core.ResourceURI; import com.b2international.snowowl.core.branch.Branch; import com.b2international.snowowl.core.codesystem.CodeSystem; @@ -536,7 +536,7 @@ public void codesystem27_GetWithTimestamp() { } @Test - public void codeSystem28_SearchWithTimestamp() throws Exception { + public void codesystem28_SearchWithTimestamp() throws Exception { assertCodeSystemCreated(prepareCodeSystemCreateRequestBody("cs28_1")); assertCodeSystemCreated(prepareCodeSystemCreateRequestBody("cs28_2")); assertCodeSystemCreated(prepareCodeSystemCreateRequestBody("cs28_3")); @@ -555,7 +555,7 @@ public void codeSystem28_SearchWithTimestamp() throws Exception { } @Test - public void codeSystem29_VersionWithReservedBranchName() throws Exception { + public void codesystem29_VersionWithReservedBranchName() throws Exception { String codeSystemId = assertCodeSystemCreated(prepareCodeSystemCreateRequestBody("cs29_1")); assertVersionCreated(prepareVersionCreateRequestBody(CodeSystem.uri(codeSystemId), ResourceURI.HEAD, EffectiveTimes.today())) .statusCode(400) @@ -567,7 +567,35 @@ public void codeSystem29_VersionWithReservedBranchName() throws Exception { .statusCode(400) .body("message", containsString("Version 'NEXT' is a reserved alias or branch name.")); } - + + @Test + public void codesystem30_AllowMetadataUpdatesOnRetiredResources() throws Exception { + String codeSystemId = assertCodeSystemCreated(prepareCodeSystemCreateRequestBody("cs30")); + assertCodeSystemUpdated(codeSystemId, Map.of( + "status", Resource.RETIRED_STATUS + )); + assertCodeSystemGet(codeSystemId) + .statusCode(200) + .body("status", equalTo(Resource.RETIRED_STATUS)); + assertCodeSystemUpdated(codeSystemId, Map.of( + "copyright", "MIT License" + )); + assertCodeSystemGet(codeSystemId) + .statusCode(200) + .body("copyright", equalTo("MIT License")); + } + + @Test + public void codesystem31_DisallowVersioningOfRetiredResources() throws Exception { + String codeSystemId = assertCodeSystemCreated(prepareCodeSystemCreateRequestBody("cs30")); + assertCodeSystemUpdated(codeSystemId, Map.of( + "status", Resource.RETIRED_STATUS + )); + assertVersionCreated(prepareVersionCreateRequestBody(CodeSystem.uri(codeSystemId), "v1", EffectiveTimes.today())) + .statusCode(400) + .body("message", containsString("Resource 'Code System cs30' cannot be versioned in its current status 'retired'.")); + } + private long getCodeSystemCreatedAt(final String id) { return assertCodeSystemGet(id) .statusCode(200) diff --git a/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/Resource.java b/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/Resource.java index 23bb18b29a9..241b9a72871 100644 --- a/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/Resource.java +++ b/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/Resource.java @@ -34,6 +34,9 @@ public abstract class Resource implements Serializable { private static final long serialVersionUID = 1L; + // known retired status value from FHIR, TODO make it configurable when needed + public static final String RETIRED_STATUS = "retired"; + /** * @since 8.0 */ diff --git a/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/context/DefaultTerminologyResourceContext.java b/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/context/DefaultTerminologyResourceContext.java index 1e6020ceb3e..cc6cd8deaba 100644 --- a/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/context/DefaultTerminologyResourceContext.java +++ b/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/context/DefaultTerminologyResourceContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 B2i Healthcare Pte Ltd, http://b2i.sg + * Copyright 2021-2022 B2i Healthcare Pte Ltd, http://b2i.sg * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ package com.b2international.snowowl.core.context; +import com.b2international.snowowl.core.Resource; import com.b2international.snowowl.core.ResourceURI; import com.b2international.snowowl.core.ServiceProvider; import com.b2international.snowowl.core.TerminologyResource; @@ -28,6 +29,7 @@ public final class DefaultTerminologyResourceContext extends DelegatingContext i public DefaultTerminologyResourceContext(final ServiceProvider delegate, ResourceURI resourceUri, TerminologyResource resource) { super(delegate); bind(ResourceURI.class, resourceUri); + bind(Resource.class, resource); bind(TerminologyResource.class, resource); } diff --git a/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/request/TerminologyResourceCommitRequestBuilder.java b/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/request/TerminologyResourceCommitRequestBuilder.java index 03045d8f433..96cc300bd8c 100644 --- a/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/request/TerminologyResourceCommitRequestBuilder.java +++ b/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/request/TerminologyResourceCommitRequestBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 B2i Healthcare Pte Ltd, http://b2i.sg + * Copyright 2021-2022 B2i Healthcare Pte Ltd, http://b2i.sg * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,12 @@ */ package com.b2international.snowowl.core.request; +import java.util.Set; + +import com.b2international.snowowl.core.Resource; import com.b2international.snowowl.core.context.TerminologyResourceContentRequestBuilder; +import com.b2international.snowowl.core.domain.BranchContext; +import com.b2international.snowowl.core.events.Request; /** * @since 8.0 @@ -24,9 +29,16 @@ public class TerminologyResourceCommitRequestBuilder extends RepositoryCommitRequestBuilder implements TerminologyResourceContentRequestBuilder { + public static final Set READ_ONLY_STATUSES = Set.of(Resource.RETIRED_STATUS); + @Override public boolean snapshot() { return false; } + + @Override + public Request wrap(Request req) { + return new TerminologyResourceStatusCheckRequest<>(TerminologyResourceContentRequestBuilder.super.wrap(req), READ_ONLY_STATUSES); + } } diff --git a/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/request/TerminologyResourceStatusCheckRequest.java b/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/request/TerminologyResourceStatusCheckRequest.java new file mode 100644 index 00000000000..1ef4ae8e0da --- /dev/null +++ b/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/request/TerminologyResourceStatusCheckRequest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2022 B2i Healthcare Pte Ltd, http://b2i.sg + * + * Licensed 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 com.b2international.snowowl.core.request; + +import java.util.Collection; +import java.util.SortedSet; + +import com.b2international.commons.exceptions.BadRequestException; +import com.b2international.snowowl.core.ServiceProvider; +import com.b2international.snowowl.core.TerminologyResource; +import com.b2international.snowowl.core.events.DelegatingRequest; +import com.b2international.snowowl.core.events.Request; +import com.google.common.collect.ImmutableSortedSet; + +/** + * @since 8.2 + * + * @param + * @param + * @param + */ +public final class TerminologyResourceStatusCheckRequest extends DelegatingRequest { + + private static final long serialVersionUID = 1L; + private final SortedSet forbiddenStatuses; + + public TerminologyResourceStatusCheckRequest(Request next, Collection forbiddenStatuses) { + super(next); + this.forbiddenStatuses = forbiddenStatuses == null ? ImmutableSortedSet.of() : ImmutableSortedSet.copyOf(forbiddenStatuses); + } + + @Override + public R execute(C context) { + TerminologyResource resource = context.service(TerminologyResource.class); + + if (forbiddenStatuses.contains(resource.getStatus())) { + throw new BadRequestException("Executing this request is forbidden on resources with one of the following states '%s'. Resource '%s' is in '%s' status.", this.forbiddenStatuses, resource.getTitle(), resource.getStatus()); + } else { + return next(context); + } + + } + +} diff --git a/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/request/version/VersionCreateRequest.java b/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/request/version/VersionCreateRequest.java index a3f368107bc..e42c2c40ea3 100644 --- a/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/request/version/VersionCreateRequest.java +++ b/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/request/version/VersionCreateRequest.java @@ -114,6 +114,10 @@ public Boolean execute(RepositoryContext context) { context.service(BranchNameValidator.class).checkName(version); TerminologyResource resourceToVersion = resourcesById.get(resource); + + if (TerminologyResourceCommitRequestBuilder.READ_ONLY_STATUSES.contains(resourceToVersion.getStatus())) { + throw new BadRequestException("Resource '%s' cannot be versioned in its current status '%s'.", resourceToVersion.getTitle(), resourceToVersion.getStatus()); + } // TODO resurrect or eliminate tooling dependencies final List resourcesToVersion = List.of(resourcesById.get(resource)); From 7d8589d1e5336fc45bb4d1b4ae094ae207d1dfbd Mon Sep 17 00:00:00 2001 From: Mark Czotter Date: Wed, 13 Apr 2022 18:02:46 +0200 Subject: [PATCH 3/5] chore(core): remove obsolete (since 7.x) branch name character `~` --- .../index/revision/RevisionBranchingTest.java | 8 -------- .../b2international/index/revision/RevisionBranch.java | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/commons/com.b2international.index.tests/src/com/b2international/index/revision/RevisionBranchingTest.java b/commons/com.b2international.index.tests/src/com/b2international/index/revision/RevisionBranchingTest.java index 169bda6c7c0..6cef47f9abb 100644 --- a/commons/com.b2international.index.tests/src/com/b2international/index/revision/RevisionBranchingTest.java +++ b/commons/com.b2international.index.tests/src/com/b2international/index/revision/RevisionBranchingTest.java @@ -108,14 +108,6 @@ public void dot() throws Exception { .isEqualTo(branchName); } - @Test - public void tilde() throws Exception { - String branchName = "~1.0"; - assertBranchCreate(branchName) - .extracting(RevisionBranch::getName) - .isEqualTo(branchName); - } - @Test(expected = BadRequestException.class) public void percent() throws Exception { String branchName = "%xy"; diff --git a/commons/com.b2international.index/src/com/b2international/index/revision/RevisionBranch.java b/commons/com.b2international.index/src/com/b2international/index/revision/RevisionBranch.java index c74bf928249..498581749bb 100644 --- a/commons/com.b2international.index/src/com/b2international/index/revision/RevisionBranch.java +++ b/commons/com.b2international.index/src/com/b2international/index/revision/RevisionBranch.java @@ -70,7 +70,7 @@ public static enum BranchState { /** * Allowed set of characters for a branch name. */ - public static final String DEFAULT_ALLOWED_BRANCH_NAME_CHARACTER_SET = "a-zA-Z0-9.~_-"; + public static final String DEFAULT_ALLOWED_BRANCH_NAME_CHARACTER_SET = "a-zA-Z0-9._-"; /** * The maximum length of a branch. From 28dffdf60128a8970f785237ea052b212ea2dbe3 Mon Sep 17 00:00:00 2001 From: Mark Czotter Date: Wed, 13 Apr 2022 18:20:52 +0200 Subject: [PATCH 4/5] feat(core): support checkBase62/chechBase64 methods in IDs helper --- .../META-INF/MANIFEST.MF | 2 +- .../b2international/snowowl/core/id/IDs.java | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/core/com.b2international.snowowl.core/META-INF/MANIFEST.MF b/core/com.b2international.snowowl.core/META-INF/MANIFEST.MF index d46c011cc3a..e64b0996897 100644 --- a/core/com.b2international.snowowl.core/META-INF/MANIFEST.MF +++ b/core/com.b2international.snowowl.core/META-INF/MANIFEST.MF @@ -29,7 +29,7 @@ Require-Bundle: org.eclipse.core.runtime;bundle-version="3.9.0", io.github.classgraph.classgraph;bundle-version="4.8.90", com.b2international.groovy;bundle-version="3.0.9", com.b2international.snomed.ecl;bundle-version="1.6.0";visibility:=reexport, - com.github.f4b6a3.uuid;bundle-version="4.6.1" + com.github.f4b6a3.uuid;bundle-version="4.6.1";visibility:=reexport Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Export-Package: com.auth0.jwt, diff --git a/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/id/IDs.java b/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/id/IDs.java index cb4270df177..a337e02f477 100644 --- a/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/id/IDs.java +++ b/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/id/IDs.java @@ -15,11 +15,14 @@ */ package com.b2international.snowowl.core.id; +import java.util.Arrays; import java.util.UUID; import org.elasticsearch.common.UUIDs; +import com.b2international.commons.exceptions.BadRequestException; import com.github.f4b6a3.uuid.codec.base.Base62Codec; +import com.github.f4b6a3.uuid.codec.base.BaseN; import com.google.common.base.Charsets; import com.google.common.hash.Hashing; @@ -30,6 +33,9 @@ */ public class IDs { + public static final BaseN BASE64 = new BaseN("A-Za-z0-9-_"); + public static final BaseN BASE62 = new BaseN("A-Za-z0-9"); + /** * Generates a time-based UUID (similar to Flake IDs), which is preferred when generating an ID to be indexed into a Lucene index as primary key. This methods uses Base 62 encoding of UUIDs to omit the usage of non-alpha/numeric characters entirely. * @@ -81,4 +87,18 @@ public static String sha1(String value) { return Hashing.sha1().hashString(value, Charsets.UTF_8).toString(); } + public static void checkBase62(String value, String property) { + checkBaseN(value, BASE62, value); + } + + public static void checkBase64(String value, String property) { + checkBaseN(value, BASE64, value); + } + + public static void checkBaseN(String value, BaseN baseN, String property) { + if (baseN.isValid(value)) { + throw new BadRequestException("%s'%s' uses an illegal character. Allowed characters are '%s'.", property == null ? "" : property.concat(" "), Arrays.toString(BASE62.getAlphabet().array())); + } + } + } From 1c9b5ceb86f5a75b4da00f0fff92dc216472b2f6 Mon Sep 17 00:00:00 2001 From: Mark Czotter Date: Wed, 13 Apr 2022 18:21:32 +0200 Subject: [PATCH 5/5] fix(core): allow only base64 character set for resource.id fields --- .../b2international/snowowl/core/id/IDs.java | 20 ++++++++++--------- .../request/BaseResourceCreateRequest.java | 13 ++++-------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/id/IDs.java b/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/id/IDs.java index a337e02f477..f28b8c43764 100644 --- a/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/id/IDs.java +++ b/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/id/IDs.java @@ -15,14 +15,13 @@ */ package com.b2international.snowowl.core.id; -import java.util.Arrays; import java.util.UUID; +import java.util.regex.Pattern; import org.elasticsearch.common.UUIDs; import com.b2international.commons.exceptions.BadRequestException; import com.github.f4b6a3.uuid.codec.base.Base62Codec; -import com.github.f4b6a3.uuid.codec.base.BaseN; import com.google.common.base.Charsets; import com.google.common.hash.Hashing; @@ -33,8 +32,11 @@ */ public class IDs { - public static final BaseN BASE64 = new BaseN("A-Za-z0-9-_"); - public static final BaseN BASE62 = new BaseN("A-Za-z0-9"); + public static final String BASE62_CHARSET = "A-Za-z0-9"; + public static final String BASE64_CHARSET = "A-Za-z0-9-_"; + + public static final Pattern BASE62 = Pattern.compile("["+ BASE62_CHARSET +"]+"); + public static final Pattern BASE64 = Pattern.compile("["+ BASE64_CHARSET +"]+"); /** * Generates a time-based UUID (similar to Flake IDs), which is preferred when generating an ID to be indexed into a Lucene index as primary key. This methods uses Base 62 encoding of UUIDs to omit the usage of non-alpha/numeric characters entirely. @@ -88,16 +90,16 @@ public static String sha1(String value) { } public static void checkBase62(String value, String property) { - checkBaseN(value, BASE62, value); + checkBaseN(value, property, BASE62, BASE62_CHARSET); } public static void checkBase64(String value, String property) { - checkBaseN(value, BASE64, value); + checkBaseN(value, property, BASE64, BASE64_CHARSET); } - public static void checkBaseN(String value, BaseN baseN, String property) { - if (baseN.isValid(value)) { - throw new BadRequestException("%s'%s' uses an illegal character. Allowed characters are '%s'.", property == null ? "" : property.concat(" "), Arrays.toString(BASE62.getAlphabet().array())); + private static void checkBaseN(String value, String property, Pattern pattern, String allowedCharacterSet) { + if (!pattern.matcher(value).matches()) { + throw new BadRequestException("%s'%s' uses an illegal character. Allowed characters are '%s'.", property == null ? "" : property.concat(" "), value, allowedCharacterSet); } } diff --git a/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/request/BaseResourceCreateRequest.java b/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/request/BaseResourceCreateRequest.java index b5578822ccb..4bca19f14be 100644 --- a/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/request/BaseResourceCreateRequest.java +++ b/core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/request/BaseResourceCreateRequest.java @@ -21,15 +21,14 @@ import org.hibernate.validator.constraints.NotEmpty; import com.b2international.commons.exceptions.AlreadyExistsException; -import com.b2international.commons.exceptions.BadRequestException; import com.b2international.commons.exceptions.NotFoundException; -import com.b2international.index.revision.RevisionBranch.BranchNameValidator; import com.b2international.snowowl.core.ServiceProvider; import com.b2international.snowowl.core.bundle.Bundle; import com.b2international.snowowl.core.bundle.Bundles; import com.b2international.snowowl.core.domain.IComponent; import com.b2international.snowowl.core.domain.TransactionContext; import com.b2international.snowowl.core.events.Request; +import com.b2international.snowowl.core.id.IDs; import com.b2international.snowowl.core.identity.User; import com.b2international.snowowl.core.internal.ResourceDocument; import com.b2international.snowowl.core.internal.ResourceDocument.Builder; @@ -43,7 +42,7 @@ public abstract class BaseResourceCreateRequest implements Request { protected static final long serialVersionUID = 1L; - + // the new ID, if not specified, it will be auto-generated @JsonProperty @NotEmpty @@ -184,12 +183,8 @@ protected final void setPurpose(String purpose) { @Override public final String execute(TransactionContext context) { // validate ID before use, IDs sometimes being used as branch paths, so must be a valid branch path - try { - context.service(BranchNameValidator.class).checkName(id); - } catch (BadRequestException e) { - throw new BadRequestException(e.getMessage().replace("Branch name", getClass().getSimpleName().replace("CreateRequest", ".id"))); - } - + IDs.checkBase64(id, getClass().getSimpleName().replace("CreateRequest", ".id")); + // validate URL format getResourceURLSchemaSupport(context).validate(url);