exceptionfactory commented on code in PR #10322:
URL: https://github.com/apache/nifi/pull/10322#discussion_r2474825803
##########
nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/StandardMultipartFormDataStreamBuilder.java:
##########
@@ -55,30 +55,29 @@ public class StandardMultipartFormDataStreamBuilder
implements MultipartFormData
private final List<Part> parts = new ArrayList<>();
- /**
- * Build Sequence Input Stream from collection of Form Data Parts
formatted with boundaries
- *
- * @return Input Stream
- */
@Override
public InputStream build() {
if (parts.isEmpty()) {
throw new IllegalStateException("Parts required");
}
- final List<InputStream> partInputStreams = new ArrayList<>();
+ final List<InputStream> streams = new ArrayList<>();
+ for (int index = 0; index < parts.size(); index++) {
+ final Part part = parts.get(index);
+ final String boundaryPrefix = (index == 0 ? BOUNDARY_SEPARATOR +
boundary + CARRIAGE_RETURN_LINE_FEED
+ : CARRIAGE_RETURN_LINE_FEED + BOUNDARY_SEPARATOR +
boundary + CARRIAGE_RETURN_LINE_FEED);
Review Comment:
This ternary and string concatenation is rather difficult to read. Having
something like the `getFooter` method would be helpful.
##########
nifi-extension-bundles/nifi-atlassian-bundle/nifi-atlassian-extensions/src/main/java/org/apache/nifi/atlassian/bitbucket/BitbucketRepositoryClient.java:
##########
@@ -299,13 +419,43 @@ public String createContent(final GitCreateContentRequest
request) throws FlowRe
String.format("Error while committing content for
repository [%s] on branch %s at path %s: %s", repoName, branch, resolvedPath,
getErrorMessage(response)));
}
- final Optional<String> lastCommit = getLatestCommit(branch,
resolvedPath);
+ return getRequiredLatestCommit(branch, resolvedPath);
+ }
- if (lastCommit.isEmpty()) {
- throw new FlowRegistryException(String.format("Could not find
commit for the file %s we just tried to commit on branch %s", resolvedPath,
branch));
+ private String createContentDataCenter(final GitCreateContentRequest
request, final String resolvedPath, final String branch) throws
FlowRegistryException {
+ final StandardMultipartFormDataStreamBuilder multipartBuilder = new
StandardMultipartFormDataStreamBuilder();
+ final String fileName = getFileName(resolvedPath);
+ multipartBuilder.addPart("content", fileName,
StandardHttpContentType.APPLICATION_OCTET_STREAM,
request.getContent().getBytes(StandardCharsets.UTF_8));
+ multipartBuilder.addPart("branch", StandardHttpContentType.TEXT_PLAIN,
branch.getBytes(StandardCharsets.UTF_8));
+
+ final String message = request.getMessage();
+ if (message != null && !message.isEmpty()) {
+ multipartBuilder.addPart("message",
StandardHttpContentType.TEXT_PLAIN, message.getBytes(StandardCharsets.UTF_8));
}
- return lastCommit.get();
+ final String existingContentSha = request.getExistingContentSha();
+ if (existingContentSha != null && !existingContentSha.isEmpty()) {
+ multipartBuilder.addPart("sourceCommitId",
StandardHttpContentType.TEXT_PLAIN,
existingContentSha.getBytes(StandardCharsets.UTF_8));
+ }
+
+ final HttpUriBuilder uriBuilder =
getRepositoryUriBuilder().addPathSegment("browse");
+ addPathSegments(uriBuilder, resolvedPath);
+ final URI uri = uriBuilder.build();
+ final byte[] requestBody = toByteArray(multipartBuilder.build());
+ final HttpResponseEntity response =
this.webClient.getWebClientService()
+ .put()
+ .uri(uri)
+ .body(new ByteArrayInputStream(requestBody),
OptionalLong.of(requestBody.length))
+ .header(AUTHORIZATION_HEADER, authToken.getAuthzHeaderValue())
+ .header(CONTENT_TYPE_HEADER,
multipartBuilder.getHttpContentType().getContentType())
+ .retrieve();
+
+ if (response.statusCode() != HttpURLConnection.HTTP_OK &&
response.statusCode() != HttpURLConnection.HTTP_CREATED) {
Review Comment:
Is it possible to return either 200 or 201? It seems like it should be one
or the other.
##########
nifi-extension-bundles/nifi-atlassian-bundle/nifi-atlassian-extensions/src/main/java/org/apache/nifi/atlassian/bitbucket/BitbucketFlowRegistryClient.java:
##########
@@ -160,7 +174,17 @@ public boolean
isStorageLocationApplicable(FlowRegistryClientConfigurationContex
@Override
protected String getStorageLocation(GitRepositoryClient repositoryClient) {
- final BitbucketRepositoryClient gitLabRepositoryClient =
(BitbucketRepositoryClient) repositoryClient;
- return
STORAGE_LOCATION_FORMAT.formatted(gitLabRepositoryClient.getWorkspace(),
gitLabRepositoryClient.getRepoName());
+ final BitbucketRepositoryClient bitbucketRepositoryClient =
(BitbucketRepositoryClient) repositoryClient;
+
+ if (bitbucketRepositoryClient.getFormFactor() ==
BitbucketFormFactor.DATA_CENTER) {
+ final String apiHost = bitbucketRepositoryClient.getApiHost();
+ final String projectKey =
bitbucketRepositoryClient.getProjectKey();
+ if (apiHost != null && projectKey != null) {
+ return "git@" + apiHost + ":" + projectKey + "/" +
bitbucketRepositoryClient.getRepoName() + ".git";
Review Comment:
Recommend using string formatting instead of concatenation to make this
structure easier to follow.
##########
nifi-extension-bundles/nifi-atlassian-bundle/nifi-atlassian-extensions/src/main/java/org/apache/nifi/atlassian/bitbucket/BitbucketFlowRegistryClient.java:
##########
@@ -92,7 +101,8 @@ public class BitbucketFlowRegistryClient extends
AbstractGitFlowRegistryClient {
static final PropertyDescriptor APP_PASSWORD = new
PropertyDescriptor.Builder()
.name("App Password")
- .description("The App Password to use for authentication")
+ .displayName("App Password or API Token")
Review Comment:
Instead of adding the `displayName`, it would be better to migrate the
property name. Alternatively, for the sake of clarity, it might be better to
have a separate property.
##########
nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/MultipartFormDataStreamBuilder.java:
##########
@@ -55,4 +55,12 @@ public interface MultipartFormDataStreamBuilder {
* @return Builder
*/
MultipartFormDataStreamBuilder addPart(String name, HttpContentType
httpContentType, byte[] bytes);
+
+ default MultipartFormDataStreamBuilder addPart(String name, String
fileName, HttpContentType httpContentType, InputStream inputStream) {
+ return addPart(name, httpContentType, inputStream);
+ }
+
+ default MultipartFormDataStreamBuilder addPart(String name, String
fileName, HttpContentType httpContentType, byte[] bytes) {
+ return addPart(name, httpContentType, bytes);
+ }
Review Comment:
For completeness, it would be helpful to add JavaDoc comments like the other
methods.
##########
nifi-extension-bundles/nifi-atlassian-bundle/nifi-atlassian-extensions/src/main/java/org/apache/nifi/atlassian/bitbucket/BitbucketAuthenticationType.java:
##########
@@ -21,8 +21,9 @@
public enum BitbucketAuthenticationType implements DescribedValue {
- BASIC_AUTH("Basic Auth", """
+ BASIC_AUTH("Basic Auth & API Token", """
Review Comment:
Recommend avoiding the use of the ampersand character in labels. On the
other hand, is it necessary to mention `API Token` in the name when it is in
the description?
```suggestion
BASIC_AUTH("Basic Auth and API Token", """
```
##########
nifi-extension-bundles/nifi-atlassian-bundle/nifi-atlassian-extensions/src/main/java/org/apache/nifi/atlassian/bitbucket/BitbucketRepositoryClient.java:
##########
@@ -334,34 +493,249 @@ public InputStream deleteContent(final String filePath,
final String commitMessa
throw new FlowRegistryException(
String.format("Error while deleting content for repository
[%s] on branch %s at path %s: %s", repoName, branch, resolvedPath,
getErrorMessage(response)));
}
+ }
- return fileToBeDeleted;
+ private void deleteContentDataCenter(final String resolvedPath, final
String commitMessage, final String branch) throws FlowRegistryException {
+ final Optional<String> latestCommit = getLatestCommit(branch,
resolvedPath);
+ final HttpUriBuilder uriBuilder =
getRepositoryUriBuilder().addPathSegment("browse");
+ addPathSegments(uriBuilder, resolvedPath);
+ uriBuilder.addQueryParameter("branch", branch);
+ if (commitMessage != null) {
+ uriBuilder.addQueryParameter("message", commitMessage);
+ }
+ latestCommit.ifPresent(commit ->
uriBuilder.addQueryParameter("sourceCommitId", commit));
+
+ final URI uri = uriBuilder.build();
+ final HttpResponseEntity response =
this.webClient.getWebClientService()
+ .delete()
+ .uri(uri)
+ .header(AUTHORIZATION_HEADER, authToken.getAuthzHeaderValue())
+ .retrieve();
+
+ final int statusCode = response.statusCode();
+ if (statusCode != HttpURLConnection.HTTP_OK && statusCode !=
HttpURLConnection.HTTP_ACCEPTED
+ && statusCode != HttpURLConnection.HTTP_NO_CONTENT &&
statusCode != HttpURLConnection.HTTP_CREATED) {
+ throw new FlowRegistryException(
+ String.format("Error while deleting content for repository
[%s] on branch %s at path %s: %s", repoName, branch, resolvedPath,
getErrorMessage(response)));
+ }
}
private Iterator<JsonNode> getFiles(final String branch, final String
resolvedPath) throws FlowRegistryException {
final Optional<String> lastCommit = getLatestCommit(branch,
resolvedPath);
if (lastCommit.isEmpty()) {
- throw new FlowRegistryException(String.format("Could not find
committed files at %s on branch %s response from Bitbucket API", resolvedPath,
branch));
+ return Collections.emptyIterator();
}
- // retrieve source data
- //
https://api.bitbucket.org/2.0/repositories/{workspace}/{repoName}/src/{commit}/{path}
- final URI uri =
getUriBuilder().addPathSegment("src").addPathSegment(lastCommit.get()).addPathSegment(resolvedPath).build();
+ return formFactor == BitbucketFormFactor.DATA_CENTER
+ ? getFilesDataCenter(branch, resolvedPath, lastCommit.get())
+ : getFilesCloud(branch, resolvedPath, lastCommit.get());
+ }
+
+ private Iterator<JsonNode> getFilesCloud(final String branch, final String
resolvedPath, final String commit) throws FlowRegistryException {
+ final URI uri = getRepositoryUriBuilder()
+ .addPathSegment("src")
+ .addPathSegment(commit)
+ .addPathSegment(resolvedPath)
+ .build();
final String errorMessage = String.format("Error while listing content
for repository [%s] on branch %s at path %s", repoName, branch, resolvedPath);
return getPagedResponseValues(uri, errorMessage);
}
+ private Iterator<JsonNode> getFilesDataCenter(final String branch, final
String resolvedPath, final String commit) throws FlowRegistryException {
+ final List<JsonNode> allValues = new ArrayList<>();
+ Integer nextPageStart = null;
+
+ do {
+ final URI uri = buildBrowseUri(resolvedPath, commit,
nextPageStart, false);
+ final HttpResponseEntity response =
this.webClient.getWebClientService()
+ .get()
+ .uri(uri)
+ .header(AUTHORIZATION_HEADER,
authToken.getAuthzHeaderValue())
+ .retrieve();
+
+ if (response.statusCode() != HttpURLConnection.HTTP_OK) {
+ final String errorMessage = String.format("Error while listing
content for repository [%s] on branch %s at path %s", repoName, branch,
resolvedPath);
+ throw new FlowRegistryException(errorMessage + ": " +
getErrorMessage(response));
+ }
+
+ final JsonNode root;
+ try {
+ root = objectMapper.readTree(response.body());
+ } catch (IOException e) {
+ throw new FlowRegistryException(String.format("Could not parse
Bitbucket API response at %s", uri), e);
+ }
+
+ final JsonNode children = root.path("children");
+ final JsonNode values = children.path("values");
+ if (values.isArray()) {
+ values.forEach(allValues::add);
+ }
+
+ if (children.path("isLastPage").asBoolean(true)) {
+ nextPageStart = null;
+ } else {
+ final JsonNode nextPageStartNode =
children.get("nextPageStart");
+ nextPageStart = nextPageStartNode != null &&
nextPageStartNode.isInt() ? nextPageStartNode.intValue() : null;
+ }
+ } while (nextPageStart != null);
+
+ return allValues.iterator();
+ }
+
+ private URI buildBrowseUri(final String resolvedPath, final String commit,
final Integer start, final boolean rawContent) {
+ final HttpUriBuilder builder =
getRepositoryUriBuilder().addPathSegment("browse");
+ addPathSegments(builder, resolvedPath);
+ builder.addQueryParameter("at", commit);
+ if (start != null) {
+ builder.addQueryParameter("start", String.valueOf(start));
+ }
+ return builder.build();
+ }
+
+ private URI buildRawUri(final String resolvedPath, final String commit) {
+ final HttpUriBuilder builder =
getRepositoryUriBuilder().addPathSegment("raw");
+ addPathSegments(builder, resolvedPath);
+ builder.addQueryParameter("at", commit);
+ return builder.build();
+ }
+
private Iterator<JsonNode> getListCommits(final String branch, final
String path) throws FlowRegistryException {
- // retrieve latest commit for that branch
- //
https://api.bitbucket.org/2.0/repositories/{workspace}/{repoName}/commits/{branch}
- final URI uri =
getUriBuilder().addPathSegment("commits").addPathSegment(branch).addQueryParameter("path",
path).build();
- final String errorMessage = String.format("Error while listing commits
for repository [%s] on branch %s", repoName, branch);
+ if (formFactor == BitbucketFormFactor.DATA_CENTER) {
+ return getListCommitsDataCenter(branch, path);
+ }
+ return getListCommitsCloud(branch, path);
+ }
+
+ private Iterator<JsonNode> getListCommitsCloud(final String branch, final
String path) throws FlowRegistryException {
+ if (!hasCommits) {
+ // very specific behavior when there is no commit at all yet in
the repo
+ final URI uri = getRepositoryUriBuilder()
+ .addPathSegment("commits")
+ .build();
+
+ final HttpResponseEntity response = webClient.getWebClientService()
+ .get()
+ .uri(uri)
+ .header(AUTHORIZATION_HEADER,
authToken.getAuthzHeaderValue())
+ .retrieve();
+
+ if (response.statusCode() != HttpURLConnection.HTTP_OK) {
+ final String errorMessage = String.format("Error while listing
commits for repository [%s] on branch %s", repoName, branch);
+ throw new FlowRegistryException(errorMessage + ": " +
getErrorMessage(response));
+ }
+
+ final JsonNode root;
+ try {
+ root = objectMapper.readTree(response.body());
+ } catch (IOException e) {
+ throw new FlowRegistryException(String.format("Could not parse
Bitbucket API response at %s", uri), e);
+ }
+
+ final JsonNode values = root.path("values");
+ if (values.isArray() && values.isEmpty()) {
+ return Collections.emptyIterator();
+ } else {
+ // There is at least one commit, proceed as usual
+ // and never check again
+ hasCommits = true;
+ }
+ }
+
+ final URI uri = getRepositoryUriBuilder()
+ .addPathSegment("commits")
+ .addPathSegment(branch)
+ .addQueryParameter("path", path)
+ .build();
+ final String errorMessage = String.format("Error while listing commits
for repository [%s] on branch %s", repoName, branch);
return getPagedResponseValues(uri, errorMessage);
}
+ private Iterator<JsonNode> getListCommitsDataCenter(final String branch,
final String path) throws FlowRegistryException {
+ final List<JsonNode> allValues = new ArrayList<>();
+ Integer nextPageStart = null;
+
+ do {
+ if (!hasCommits) {
+ // very specific behavior when there is no commit at all yet
in the repo
+ final URI uri = buildCommitsUri(null, null, null);
+ final HttpResponseEntity response =
this.webClient.getWebClientService()
+ .get()
+ .uri(uri)
+ .header(AUTHORIZATION_HEADER,
authToken.getAuthzHeaderValue())
+ .retrieve();
+ if (response.statusCode() != HttpURLConnection.HTTP_OK) {
+ final String errorMessage = String.format("Error while
listing commits for repository [%s] on branch %s", repoName, branch);
+ throw new FlowRegistryException(errorMessage + ": " +
getErrorMessage(response));
+ }
+ final JsonNode root;
+ try {
+ root = objectMapper.readTree(response.body());
+ } catch (IOException e) {
+ throw new FlowRegistryException(String.format("Could not
parse Bitbucket API response at %s", uri), e);
+ }
+
+ final JsonNode values = root.path("values");
+ if (values.isArray() && values.isEmpty()) {
+ return Collections.emptyIterator();
+ } else {
+ // There is at least one commit, proceed as usual
+ // and never check again
+ hasCommits = true;
+ }
+ }
+
+ final URI uri = buildCommitsUri(branch, path, nextPageStart);
+ final HttpResponseEntity response =
this.webClient.getWebClientService()
+ .get()
+ .uri(uri)
+ .header(AUTHORIZATION_HEADER,
authToken.getAuthzHeaderValue())
+ .retrieve();
+
+ if (response.statusCode() != HttpURLConnection.HTTP_OK) {
+ final String errorMessage = String.format("Error while listing
commits for repository [%s] on branch %s", repoName, branch);
+ throw new FlowRegistryException(errorMessage + ": " +
getErrorMessage(response));
+ }
+
+ final JsonNode root;
+ try {
+ root = objectMapper.readTree(response.body());
+ } catch (IOException e) {
+ throw new FlowRegistryException(String.format("Could not parse
Bitbucket API response at %s", uri), e);
+ }
+
+ final JsonNode values = root.path("values");
Review Comment:
It looks like there are a number of repeated uses of certain expected field
names, which could be useful to declare as static final variables.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]