1 package io.apicurio.registry.maven;
2
3 import com.fasterxml.jackson.databind.JsonNode;
4 import com.fasterxml.jackson.databind.ObjectMapper;
5 import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
6 import com.microsoft.kiota.ApiException;
7 import io.apicurio.registry.content.ContentHandle;
8 import io.apicurio.registry.content.TypedContent;
9 import io.apicurio.registry.content.refs.ExternalReference;
10 import io.apicurio.registry.content.refs.ReferenceFinder;
11 import io.apicurio.registry.maven.refs.IndexedResource;
12 import io.apicurio.registry.maven.refs.ReferenceIndex;
13 import io.apicurio.registry.rest.client.RegistryClient;
14 import io.apicurio.registry.rest.client.models.*;
15 import io.apicurio.registry.types.ArtifactType;
16 import io.apicurio.registry.types.ContentTypes;
17 import io.apicurio.registry.types.provider.ArtifactTypeUtilProvider;
18 import io.apicurio.registry.types.provider.DefaultArtifactTypeUtilProviderImpl;
19 import io.vertx.core.Vertx;
20 import org.apache.commons.io.FileUtils;
21 import org.apache.maven.plugin.MojoExecutionException;
22 import org.apache.maven.plugin.MojoFailureException;
23 import org.apache.maven.plugins.annotations.Mojo;
24 import org.apache.maven.plugins.annotations.Parameter;
25
26 import java.io.*;
27 import java.net.URI;
28 import java.nio.charset.StandardCharsets;
29 import java.nio.file.Files;
30 import java.nio.file.Path;
31 import java.nio.file.Paths;
32 import java.util.*;
33 import java.util.concurrent.ExecutionException;
34 import java.util.stream.Collectors;
35 import java.util.regex.Matcher;
36 import java.util.regex.Pattern;
37
38
39
40
41 @Mojo(name = "register", requiresProject = false)
42 public class RegisterRegistryMojo extends AbstractRegistryMojo {
43
44
45
46
47 @Parameter(required = false)
48 List<ExistingReference> existingReferences;
49
50
51
52
53 @Parameter(required = false)
54 List<RegisterArtifact> artifacts;
55
56
57
58
59 @Parameter(property = "skipRegister", defaultValue = "false")
60 boolean skip;
61
62
63
64
65
66 @Parameter(property = "dryRun", defaultValue = "false")
67 boolean dryRun;
68
69 private RegisterArtifact.AvroAutoRefsNamingStrategy avroAutoRefsNamingStrategy;
70
71 DefaultArtifactTypeUtilProviderImpl utilProviderFactory = new DefaultArtifactTypeUtilProviderImpl(true);
72 private static final String ARTIFACTS_PROPERTY_PREFIX = "artifacts.";
73 private Pattern registryArtifactUrlPattern;
74
75
76
77
78 protected boolean validate() throws MojoExecutionException {
79 loadArtifactsFromSystemPropertiesIfNeeded();
80
81 if (skip) {
82 getLog().info("register is skipped.");
83 return false;
84 }
85
86 if (artifacts == null || artifacts.isEmpty()) {
87 getLog().warn("No artifacts are configured for registration.");
88 return false;
89 }
90
91 if (existingReferences == null) {
92 existingReferences = new ArrayList<>();
93 }
94
95 int idx = 0;
96 int errorCount = 0;
97 for (RegisterArtifact artifact : artifacts) {
98 if (artifact.getGroupId() == null) {
99 getLog().error(String.format(
100 "GroupId is required when registering an artifact. Missing from artifacts[%d].",
101 idx));
102 errorCount++;
103 }
104 if (artifact.getArtifactId() == null) {
105 getLog().error(String.format(
106 "ArtifactId is required when registering an artifact. Missing from artifacts[%s].",
107 idx));
108 errorCount++;
109 }
110 if (artifact.getFile() == null) {
111 getLog().error(String.format(
112 "File is required when registering an artifact. Missing from artifacts[%s].", idx));
113 errorCount++;
114 } else if (!artifact.getFile().exists()) {
115 getLog().error(
116 String.format("Artifact file to register is configured but file does not exist: %s",
117 artifact.getFile().getPath()));
118 errorCount++;
119 }
120
121 idx++;
122 }
123
124 if (errorCount > 0) {
125 throw new MojoExecutionException(
126 "Invalid configuration of the Register Artifact(s) mojo. See the output log for details.");
127 }
128 return true;
129 }
130
131 private void loadArtifactsFromSystemPropertiesIfNeeded() throws MojoExecutionException {
132 if (artifacts != null && !artifacts.isEmpty()) {
133 return;
134 }
135 List<RegisterArtifact> cliArtifacts = parseArtifactsFromSystemProperties();
136 if (!cliArtifacts.isEmpty()) {
137 artifacts = cliArtifacts;
138 getLog().info("Loaded artifact configuration from system properties (artifacts.<index>.<field>).");
139 }
140 }
141
142 private static List<RegisterArtifact> parseArtifactsFromSystemProperties() throws MojoExecutionException {
143 Map<Integer, RegisterArtifact> artifactsByIndex = new TreeMap<>();
144
145 for (String key : System.getProperties().stringPropertyNames()) {
146 ParsedArtifactProperty parsedProperty = parseArtifactPropertyKey(key);
147 if (parsedProperty == null) {
148 continue;
149 }
150
151 String value = System.getProperty(key);
152
153 RegisterArtifact artifact = artifactsByIndex.computeIfAbsent(parsedProperty.index,
154 idx -> new RegisterArtifact());
155 applyCliArtifactField(artifact, parsedProperty.field, value, key);
156 }
157
158 return new ArrayList<>(artifactsByIndex.values());
159 }
160
161 private static ParsedArtifactProperty parseArtifactPropertyKey(String key) throws MojoExecutionException {
162 if (!key.startsWith(ARTIFACTS_PROPERTY_PREFIX)) {
163 return null;
164 }
165
166 String remainder = key.substring(ARTIFACTS_PROPERTY_PREFIX.length());
167 if (remainder.isEmpty()) {
168 return null;
169 }
170
171 int firstDotIdx = remainder.indexOf('.');
172 if (firstDotIdx == -1) {
173 return new ParsedArtifactProperty(0, remainder);
174 }
175
176 String firstSegment = remainder.substring(0, firstDotIdx);
177 String field = remainder.substring(firstDotIdx + 1);
178 if (field.isEmpty()) {
179 return null;
180 }
181
182 if (isAllDigits(firstSegment)) {
183 return new ParsedArtifactProperty(Integer.parseInt(firstSegment), field);
184 }
185
186 return new ParsedArtifactProperty(0, remainder);
187 }
188
189 private static boolean isAllDigits(String value) {
190 for (int idx = 0; idx < value.length(); idx++) {
191 if (!Character.isDigit(value.charAt(idx))) {
192 return false;
193 }
194 }
195 return !value.isEmpty();
196 }
197
198 private static final class ParsedArtifactProperty {
199 private final int index;
200 private final String field;
201
202 private ParsedArtifactProperty(int index, String field) {
203 this.index = index;
204 this.field = field;
205 }
206 }
207
208 private static void applyCliArtifactField(RegisterArtifact artifact, String field, String value, String propertyKey)
209 throws MojoExecutionException {
210 ParsedListProperty parsedListProperty = parseListProperty(field);
211 if (parsedListProperty != null) {
212 applyCliArtifactListField(artifact, parsedListProperty, value, propertyKey);
213 return;
214 }
215
216 switch (field) {
217 case "groupId":
218 artifact.setGroupId(value);
219 break;
220 case "artifactId":
221 artifact.setArtifactId(value);
222 break;
223 case "artifactType":
224 artifact.setArtifactType(value);
225 break;
226 case "file":
227 artifact.setFile(value == null ? null : new File(value).getAbsoluteFile());
228 break;
229 case "ifExists":
230 artifact.setIfExists(parseIfExists(value, propertyKey));
231 break;
232 case "canonicalize":
233 artifact.setCanonicalize(Boolean.valueOf(value));
234 break;
235 case "minify":
236 artifact.setMinify(Boolean.valueOf(value));
237 break;
238 case "autoRefs":
239 artifact.setAutoRefs(Boolean.valueOf(value));
240 break;
241 case "isDraft":
242 artifact.setIsDraft(Boolean.valueOf(value));
243 break;
244 case "contentType":
245 artifact.setContentType(value);
246 break;
247 case "version":
248 artifact.setVersion(value);
249 break;
250 case "versionStrategy":
251 try {
252 artifact.setVersionStrategy(RegisterArtifact.VersionStrategy.valueOf(value));
253 } catch (IllegalArgumentException e) {
254 throw new MojoExecutionException("Invalid value for " + propertyKey + ": " + value
255 + ". Allowed values are: "
256 + Arrays.toString(RegisterArtifact.VersionStrategy.values()));
257 }
258 break;
259 case "avroAutoRefsNamingStrategy":
260 try {
261 artifact.setAvroAutoRefsNamingStrategy(RegisterArtifact.AvroAutoRefsNamingStrategy.valueOf(value));
262 } catch (IllegalArgumentException e) {
263 throw new MojoExecutionException("Invalid value for " + propertyKey + ": " + value
264 + ". Allowed values are: "
265 + Arrays.toString(RegisterArtifact.AvroAutoRefsNamingStrategy.values()));
266 }
267 break;
268 default:
269 throw new MojoExecutionException("Unsupported CLI property for artifact configuration: "
270 + propertyKey + ". Supported fields include groupId, artifactId, artifactType, file, ifExists, "
271 + "canonicalize, minify, autoRefs, isDraft, contentType, version, "
272 + "versionStrategy, avroAutoRefsNamingStrategy, references.<index>.<field>, "
273 + "existingReferences.<index>.<field>, protoPaths.<index>.");
274 }
275 }
276
277 private static void applyCliArtifactListField(RegisterArtifact artifact, ParsedListProperty parsedListProperty,
278 String value, String propertyKey) throws MojoExecutionException {
279 switch (parsedListProperty.listName) {
280 case "references":
281 RegisterArtifactReference reference = getOrCreateListItem(artifact.getReferences(),
282 artifact::setReferences, parsedListProperty.index, RegisterArtifactReference::new);
283 if (parsedListProperty.field == null) {
284 throw new MojoExecutionException("Missing field for reference configuration: " + propertyKey);
285 }
286 if ("name".equals(parsedListProperty.field)) {
287 reference.setName(value);
288 } else {
289 applyCliArtifactField(reference, parsedListProperty.field, value, propertyKey);
290 }
291 break;
292 case "existingReferences":
293 ExistingReference existingReference = getOrCreateListItem(artifact.getExistingReferences(),
294 artifact::setExistingReferences, parsedListProperty.index, ExistingReference::new);
295 if (parsedListProperty.field == null) {
296 throw new MojoExecutionException("Missing field for existingReference configuration: " + propertyKey);
297 }
298 applyCliExistingReferenceField(existingReference, parsedListProperty.field, value, propertyKey);
299 break;
300 case "protoPaths":
301 if (parsedListProperty.field != null) {
302 throw new MojoExecutionException("Unsupported protoPaths CLI property: " + propertyKey
303 + ". Use protoPaths.<index>=<path>.");
304 }
305 List<File> protoPaths = artifact.getProtoPaths();
306 if (protoPaths == null) {
307 protoPaths = new ArrayList<>();
308 artifact.setProtoPaths(protoPaths);
309 }
310 ensureListSize(protoPaths, parsedListProperty.index);
311 protoPaths.set(parsedListProperty.index, value == null ? null : new File(value).getAbsoluteFile());
312 break;
313 default:
314 throw new MojoExecutionException("Unsupported CLI list property for artifact configuration: "
315 + propertyKey);
316 }
317 }
318
319 private static void applyCliExistingReferenceField(ExistingReference existingReference, String field, String value,
320 String propertyKey) throws MojoExecutionException {
321 switch (field) {
322 case "groupId":
323 existingReference.setGroupId(value);
324 break;
325 case "artifactId":
326 existingReference.setArtifactId(value);
327 break;
328 case "version":
329 existingReference.setVersion(value);
330 break;
331 case "resourceName":
332 existingReference.setResourceName(value);
333 break;
334 default:
335 throw new MojoExecutionException("Unsupported CLI property for existingReference configuration: "
336 + propertyKey + ". Supported fields include resourceName, groupId, artifactId, version.");
337 }
338 }
339
340 private static ParsedListProperty parseListProperty(String field) {
341 int firstDotIdx = field.indexOf('.');
342 if (firstDotIdx == -1) {
343 return null;
344 }
345
346 String listName = field.substring(0, firstDotIdx);
347 String remainder = field.substring(firstDotIdx + 1);
348 int secondDotIdx = remainder.indexOf('.');
349
350 String indexSegment = secondDotIdx == -1 ? remainder : remainder.substring(0, secondDotIdx);
351 if (!isAllDigits(indexSegment)) {
352 return null;
353 }
354
355 String nestedField = secondDotIdx == -1 ? null : remainder.substring(secondDotIdx + 1);
356 if (nestedField != null && nestedField.isEmpty()) {
357 return null;
358 }
359
360 return new ParsedListProperty(listName, Integer.parseInt(indexSegment), nestedField);
361 }
362
363 private static <T> T getOrCreateListItem(List<T> list, java.util.function.Consumer<List<T>> setter, int index,
364 java.util.function.Supplier<T> supplier) {
365 if (list == null) {
366 list = new ArrayList<>();
367 setter.accept(list);
368 }
369 ensureListSize(list, index);
370 T item = list.get(index);
371 if (item == null) {
372 item = supplier.get();
373 list.set(index, item);
374 }
375 return item;
376 }
377
378 private static <T> void ensureListSize(List<T> list, int index) {
379 while (list.size() <= index) {
380 list.add(null);
381 }
382 }
383
384 private static final class ParsedListProperty {
385 private final String listName;
386 private final int index;
387 private final String field;
388
389 private ParsedListProperty(String listName, int index, String field) {
390 this.listName = listName;
391 this.index = index;
392 this.field = field;
393 }
394 }
395
396 private record ResolvedArtifactVersion(String version, Boolean isDraft, boolean derivedFromApiInfoVersion,
397 boolean derivedFromSnapshot) {
398 private boolean shouldUpdateDraftContentOnConflict() {
399 return Boolean.TRUE.equals(isDraft) && version != null;
400 }
401
402 private boolean shouldPromoteDraftOnConflict() {
403 return derivedFromApiInfoVersion && !derivedFromSnapshot && !Boolean.TRUE.equals(isDraft)
404 && version != null;
405 }
406 }
407
408 private static io.apicurio.registry.rest.v3.beans.IfArtifactExists parseIfExists(String value, String propertyKey)
409 throws MojoExecutionException {
410 if (value == null || value.isBlank()) {
411 return null;
412 }
413 try {
414 return io.apicurio.registry.rest.v3.beans.IfArtifactExists.valueOf(value);
415 } catch (IllegalArgumentException e) {
416 for (io.apicurio.registry.rest.v3.beans.IfArtifactExists candidate
417 : io.apicurio.registry.rest.v3.beans.IfArtifactExists.values()) {
418 if (candidate.value().equalsIgnoreCase(value)) {
419 return candidate;
420 }
421 }
422 throw new MojoExecutionException("Invalid value for " + propertyKey + ": " + value
423 + ". Allowed values are: "
424 + Arrays.toString(io.apicurio.registry.rest.v3.beans.IfArtifactExists.values()));
425 }
426 }
427
428 @Override
429 protected void executeInternal() throws MojoExecutionException {
430 int errorCount = 0;
431 if (validate()) {
432 Vertx vertx = createVertx();
433 RegistryClient registryClient = createClient(vertx);
434
435 for (RegisterArtifact artifact : artifacts) {
436 String groupId = artifact.getGroupId();
437 String artifactId = artifact.getArtifactId();
438 try {
439 if (artifact.getAutoRefs() != null && artifact.getAutoRefs()) {
440
441
442 ReferenceIndex index = createIndex(artifact);
443 addExistingReferencesToIndex(registryClient, index, existingReferences);
444 addExistingReferencesToIndex(registryClient, index, artifact.getExistingReferences());
445 Stack<RegisterArtifact> registrationStack = new Stack<>();
446
447 this.avroAutoRefsNamingStrategy = artifact.getAvroAutoRefsNamingStrategy();
448 registerWithAutoRefs(registryClient, artifact, index, registrationStack);
449 } else {
450 List<ArtifactReference> references = new ArrayList<>();
451
452 if (hasReferences(artifact)) {
453 references = processArtifactReferences(registryClient, artifact.getReferences());
454 }
455 registerArtifact(registryClient, artifact, references);
456 }
457 } catch (Exception e) {
458 errorCount++;
459 getLog().error(String.format("Exception while registering artifact [%s] / [%s]", groupId,
460 artifactId), e);
461 }
462
463 }
464
465 if (errorCount > 0) {
466 throw new MojoExecutionException("Errors while registering artifacts ...");
467 }
468 }
469 }
470
471 private VersionMetaData registerWithAutoRefs(RegistryClient registryClient, RegisterArtifact artifact,
472 ReferenceIndex index, Stack<RegisterArtifact> registrationStack) throws IOException,
473 ExecutionException, InterruptedException, MojoExecutionException, MojoFailureException {
474 if (loopDetected(artifact, registrationStack)) {
475 throw new MojoExecutionException(
476 "Artifact reference loop detected (not supported): " + printLoop(registrationStack));
477 }
478 registrationStack.push(artifact);
479
480
481 ContentHandle artifactContent = readContent(artifact.getFile());
482 String artifactContentType = getContentTypeByExtension(artifact.getFile().getName());
483
484 if (artifact.getContentType() == null) {
485 artifact.setContentType(artifactContentType);
486 }
487 TypedContent typedArtifactContent = TypedContent.create(artifactContent, artifactContentType);
488
489
490 ArtifactTypeUtilProvider provider = this.utilProviderFactory
491 .getArtifactTypeProvider(artifact.getArtifactType());
492 ReferenceFinder referenceFinder = provider.getReferenceFinder();
493 var referenceArtifactIdentifierExtractor = provider.getReferenceArtifactIdentifierExtractor();
494 Set<ExternalReference> externalReferences = referenceFinder
495 .findExternalReferences(typedArtifactContent);
496
497
498 List<ArtifactReference> registeredReferences = new ArrayList<>(externalReferences.size());
499 Map<String, VersionMetaData> resolvedRegistryReferences = new HashMap<>();
500 for (ExternalReference externalRef : externalReferences) {
501 IndexedResource iresource = index.lookup(externalRef.getResource(),
502 Paths.get(artifact.getFile().toURI()));
503
504 if (iresource == null) {
505 Optional<ArtifactReference> registryReference = resolveRegistryReference(registryClient,
506 externalRef, resolvedRegistryReferences);
507 if (registryReference.isPresent()) {
508 registeredReferences.add(registryReference.get());
509 continue;
510 }
511
512 if (ReferenceUrlUtil.isAbsoluteUri(externalRef.getResource())) {
513 getLog().warn("Skipping external reference not managed by Apicurio Registry: "
514 + externalRef.getFullReference());
515 continue;
516 }
517
518 throw new MojoExecutionException("Reference could not be resolved. From: "
519 + artifact.getFile().getName() + " To: " + externalRef.getFullReference());
520 }
521
522
523 if (!iresource.isRegistered()) {
524 String groupId = artifact.getGroupId();
525
526 String artifactId = referenceArtifactIdentifierExtractor.extractArtifactId(externalRef.getResource());
527 if (ArtifactType.AVRO.equals(iresource.getType())) {
528 if (avroAutoRefsNamingStrategy == RegisterArtifact.AvroAutoRefsNamingStrategy.USE_AVRO_NAMESPACE) {
529 groupId = referenceArtifactIdentifierExtractor.extractGroupId(externalRef.getResource());
530 artifactId = referenceArtifactIdentifierExtractor.extractArtifactId(externalRef.getResource());
531 }
532 if (avroAutoRefsNamingStrategy == RegisterArtifact.AvroAutoRefsNamingStrategy.INHERIT_PARENT_GROUP) {
533 groupId = artifact.getGroupId();
534 artifactId = iresource.getResourceName();
535 }
536 }
537 File localFile = getLocalFile(iresource.getPath());
538 RegisterArtifact refArtifact = buildFromRoot(artifact, artifactId, groupId);
539 refArtifact.setArtifactType(iresource.getType());
540 refArtifact.setVersion(null);
541 refArtifact.setFile(localFile);
542 refArtifact.setContentType(getContentTypeByExtension(localFile.getName()));
543 try {
544 var car = registerWithAutoRefs(registryClient, refArtifact, index, registrationStack);
545 iresource.setRegistration(car);
546 } catch (IOException | ExecutionException | InterruptedException e) {
547 throw new RuntimeException(e);
548 }
549 }
550
551 var reference = new ArtifactReference();
552 reference.setName(externalRef.getFullReference());
553 reference.setVersion(iresource.getRegistration().getVersion());
554 reference.setGroupId(iresource.getRegistration().getGroupId());
555 reference.setArtifactId(iresource.getRegistration().getArtifactId());
556 registeredReferences.add(reference);
557 }
558 registeredReferences.sort((ref1, ref2) -> ref1.getName().compareTo(ref2.getName()));
559
560 registrationStack.pop();
561 return registerArtifact(registryClient, artifact, registeredReferences);
562 }
563
564 private Optional<ArtifactReference> resolveRegistryReference(RegistryClient registryClient,
565 ExternalReference externalRef,
566 Map<String, VersionMetaData> resolvedRegistryReferences) {
567 Optional<RegistryReferenceLocation> location = parseRegistryReferenceLocation(externalRef.getResource());
568 if (location.isEmpty()) {
569 return Optional.empty();
570 }
571
572 RegistryReferenceLocation ref = location.get();
573 VersionMetaData vmd = resolvedRegistryReferences.get(externalRef.getResource());
574 if (vmd == null) {
575 vmd = getRegistryReferenceMetadata(registryClient, ref);
576 resolvedRegistryReferences.put(externalRef.getResource(), vmd);
577 }
578 return Optional.of(buildReferenceFromMetadata(vmd,
579 ReferenceUrlUtil.registryReferenceName(externalRef.getFullReference())));
580 }
581
582 private VersionMetaData getRegistryReferenceMetadata(RegistryClient registryClient,
583 RegistryReferenceLocation ref) {
584 return registryClient.groups().byGroupId(ref.groupId).artifacts()
585 .byArtifactId(ref.artifactId).versions().byVersionExpression(ref.versionExpression).get();
586 }
587
588 private Optional<RegistryReferenceLocation> parseRegistryReferenceLocation(String resource) {
589 if (resource == null || registryUrl == null) {
590 return Optional.empty();
591 }
592
593 if (!ReferenceUrlUtil.isSameApicurioServer(registryUrl, resource)) {
594 return Optional.empty();
595 }
596
597 URI resourceUri = URI.create(resource);
598 Matcher matcher = registryArtifactUrlPattern.matcher(resourceUri.getRawPath());
599 if (!matcher.matches()) {
600 return Optional.empty();
601 }
602
603 return Optional.of(new RegistryReferenceLocation(
604 ReferenceUrlUtil.decodePathSegment(matcher.group(1)),
605 ReferenceUrlUtil.decodePathSegment(matcher.group(2)),
606 ReferenceUrlUtil.decodePathSegment(matcher.group(3))));
607 }
608
609 private static class RegistryReferenceLocation {
610 private final String groupId;
611 private final String artifactId;
612 private final String versionExpression;
613
614 private RegistryReferenceLocation(String groupId, String artifactId, String versionExpression) {
615 this.groupId = groupId;
616 this.artifactId = artifactId;
617 this.versionExpression = versionExpression;
618 }
619 }
620
621 private VersionMetaData registerArtifact(RegistryClient registryClient, RegisterArtifact artifact,
622 List<ArtifactReference> references) throws FileNotFoundException, ExecutionException,
623 InterruptedException, MojoExecutionException, MojoFailureException {
624 if (artifact.getFile() != null) {
625 return registerArtifact(registryClient, artifact, new FileInputStream(artifact.getFile()),
626 references);
627 } else {
628 return getArtifactVersionMetadata(registryClient, artifact);
629 }
630 }
631
632 private VersionMetaData getArtifactVersionMetadata(RegistryClient registryClient,
633 RegisterArtifact artifact) {
634 String groupId = artifact.getGroupId();
635 String artifactId = artifact.getArtifactId();
636 String version = artifact.getVersion();
637
638 VersionMetaData amd = registryClient.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId)
639 .versions().byVersionExpression(version).get();
640 getLog().info(String.format("Successfully processed artifact [%s] / [%s]. GlobalId is [%d]", groupId,
641 artifactId, amd.getGlobalId()));
642
643 return amd;
644 }
645
646 private VersionMetaData registerArtifact(RegistryClient registryClient, RegisterArtifact artifact,
647 InputStream artifactContent, List<ArtifactReference> references)
648 throws ExecutionException, InterruptedException, MojoFailureException, MojoExecutionException {
649 String groupId = artifact.getGroupId();
650 String artifactId = artifact.getArtifactId();
651 String type = artifact.getArtifactType();
652 Boolean canonicalize = artifact.getCanonicalize();
653 String ct = artifact.getContentType() == null ? ContentTypes.APPLICATION_JSON
654 : artifact.getContentType();
655 String data = null;
656 try {
657 if (artifact.getMinify() != null && artifact.getMinify()) {
658 ObjectMapper objectMapper = new ObjectMapper();
659 JsonNode jsonNode = objectMapper.readValue(artifactContent, JsonNode.class);
660 data = jsonNode.toString();
661 } else {
662 data = new String(artifactContent.readAllBytes(), StandardCharsets.UTF_8);
663 }
664 } catch (IOException e) {
665 throw new RuntimeException(e);
666 }
667 ResolvedArtifactVersion resolvedArtifactVersion = resolveVersion(artifact, data);
668
669 CreateArtifact createArtifact = new CreateArtifact();
670 createArtifact.setArtifactId(artifactId);
671 createArtifact.setArtifactType(type);
672
673 CreateVersion createVersion = new CreateVersion();
674 createVersion.setVersion(resolvedArtifactVersion.version());
675 createVersion.setIsDraft(resolvedArtifactVersion.isDraft());
676 createArtifact.setFirstVersion(createVersion);
677
678 VersionContent content = new VersionContent();
679 content.setContent(data);
680 content.setContentType(ct);
681 content.setReferences(references.stream().map(r -> {
682 ArtifactReference ref = new ArtifactReference();
683 ref.setArtifactId(r.getArtifactId());
684 ref.setGroupId(r.getGroupId());
685 ref.setVersion(r.getVersion());
686 ref.setName(r.getName());
687 return ref;
688 }).collect(Collectors.toList()));
689 createVersion.setContent(content);
690
691 try {
692 var vmd = registryClient.groups().byGroupId(groupId).artifacts().post(createArtifact, config -> {
693 if (artifact.getIfExists() != null) {
694 config.queryParameters.ifExists = IfArtifactExists
695 .forValue(artifact.getIfExists().value());
696 if (dryRun) {
697 config.queryParameters.dryRun = true;
698 }
699 }
700 config.queryParameters.canonical = canonicalize;
701 });
702
703 getLog().info(String.format("Successfully registered artifact [%s] / [%s]. GlobalId is [%d]",
704 groupId, artifactId, vmd.getVersion().getGlobalId()));
705
706
707 return vmd.getVersion();
708 } catch (RuleViolationProblemDetails | ProblemDetails e) {
709
710 if (e.getResponseStatusCode() == 409
711 && resolvedArtifactVersion.shouldUpdateDraftContentOnConflict()) {
712 try {
713 registryClient.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId)
714 .versions().byVersionExpression(resolvedArtifactVersion.version()).content()
715 .put(content, config -> {
716
717 });
718 getLog().info(String.format("Successfully updated artifact [%s] / [%s].",
719 groupId, artifactId));
720
721 return registryClient.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId)
722 .versions().byVersionExpression(resolvedArtifactVersion.version()).get();
723 } catch (RuleViolationProblemDetails | ProblemDetails pd) {
724 logAndThrow(pd);
725 return null;
726 }
727 } else if (e.getResponseStatusCode() == 409
728 && resolvedArtifactVersion.shouldPromoteDraftOnConflict()) {
729 return promoteExistingDraftVersion(registryClient, groupId, artifactId, content,
730 resolvedArtifactVersion, e);
731 } else {
732 logAndThrow(e);
733 return null;
734 }
735 }
736 }
737
738 private ResolvedArtifactVersion resolveVersion(RegisterArtifact artifact, String data) throws MojoExecutionException {
739 if (artifact.getVersion() != null && !artifact.getVersion().isBlank()) {
740 return new ResolvedArtifactVersion(artifact.getVersion(), artifact.getIsDraft(), false, false);
741 }
742
743 if (artifact.getVersionStrategy() == RegisterArtifact.VersionStrategy.API_INFO_VERSION
744 && isApiArtifact(artifact.getArtifactType())) {
745 String apiInfoVersion = extractApiInfoVersion(artifact, data);
746 if (apiInfoVersion == null) {
747 return new ResolvedArtifactVersion(null, artifact.getIsDraft(), false, false);
748 }
749 if (apiInfoVersion.endsWith("-SNAPSHOT")) {
750 return new ResolvedArtifactVersion(
751 apiInfoVersion.substring(0, apiInfoVersion.length() - "-SNAPSHOT".length()),
752 Boolean.TRUE,
753 true,
754 true);
755 }
756 return new ResolvedArtifactVersion(apiInfoVersion, artifact.getIsDraft(), true, false);
757 }
758
759 return new ResolvedArtifactVersion(null, artifact.getIsDraft(), false, false);
760 }
761
762 private VersionMetaData promoteExistingDraftVersion(RegistryClient registryClient, String groupId,
763 String artifactId, VersionContent content,
764 ResolvedArtifactVersion resolvedArtifactVersion,
765 ApiException registrationError)
766 throws MojoExecutionException, MojoFailureException, InterruptedException, ExecutionException {
767 try {
768 VersionMetaData existingVersion = registryClient.groups().byGroupId(groupId).artifacts()
769 .byArtifactId(artifactId).versions().byVersionExpression(resolvedArtifactVersion.version()).get();
770 if (!VersionState.DRAFT.equals(existingVersion.getState())) {
771 logAndThrow(registrationError);
772 return null;
773 }
774
775 registryClient.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).versions()
776 .byVersionExpression(resolvedArtifactVersion.version()).content().put(content, config -> {
777 });
778
779 WrappedVersionState enabled = new WrappedVersionState();
780 enabled.setState(VersionState.ENABLED);
781 registryClient.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).versions()
782 .byVersionExpression(resolvedArtifactVersion.version()).state().put(enabled);
783
784 getLog().info(String.format("Successfully promoted draft artifact [%s] / [%s] version [%s].",
785 groupId, artifactId, resolvedArtifactVersion.version()));
786
787 return registryClient.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).versions()
788 .byVersionExpression(resolvedArtifactVersion.version()).get();
789 } catch (RuleViolationProblemDetails | ProblemDetails e) {
790 logAndThrow(e);
791 return null;
792 }
793 }
794
795 private boolean isApiArtifact(String artifactType) {
796 return ArtifactType.OPENAPI.equals(artifactType) || ArtifactType.ASYNCAPI.equals(artifactType);
797 }
798
799 private String extractApiInfoVersion(RegisterArtifact artifact, String data) throws MojoExecutionException {
800 try {
801 JsonNode root = getVersionExtractionMapper(artifact).readTree(data);
802 JsonNode versionNode = root.path("info").path("version");
803 if (versionNode.isTextual()) {
804 String version = versionNode.asText();
805 if (!version.isBlank()) {
806 return version;
807 }
808 }
809 return null;
810 } catch (IOException e) {
811 throw new MojoExecutionException("Failed to extract info.version from API artifact: "
812 + artifact.getFile().getPath(), e);
813 }
814 }
815
816 private ObjectMapper getVersionExtractionMapper(RegisterArtifact artifact) {
817 return isYamlContent(artifact) ? new ObjectMapper(new YAMLFactory()) : new ObjectMapper();
818 }
819
820 private boolean isYamlContent(RegisterArtifact artifact) {
821 String contentType = artifact.getContentType();
822 if (contentType != null) {
823 String normalizedContentType = contentType.toLowerCase(Locale.ROOT);
824 if (normalizedContentType.contains("yaml") || normalizedContentType.contains("yml")) {
825 return true;
826 }
827 }
828
829 File file = artifact.getFile();
830 if (file == null) {
831 return false;
832 }
833
834 String fileName = file.getName().toLowerCase(Locale.ROOT);
835 return fileName.endsWith(".yaml") || fileName.endsWith(".yml");
836 }
837
838 private static boolean hasReferences(RegisterArtifact artifact) {
839 return artifact.getReferences() != null && !artifact.getReferences().isEmpty();
840 }
841
842 private List<ArtifactReference> processArtifactReferences(RegistryClient registryClient,
843 List<RegisterArtifactReference> referencedArtifacts) throws FileNotFoundException,
844 ExecutionException, InterruptedException, MojoExecutionException, MojoFailureException {
845 List<ArtifactReference> references = new ArrayList<>();
846 for (RegisterArtifactReference artifact : referencedArtifacts) {
847 List<ArtifactReference> nestedReferences = new ArrayList<>();
848
849
850 if (hasReferences(artifact)) {
851 nestedReferences = processArtifactReferences(registryClient, artifact.getReferences());
852 }
853 final VersionMetaData artifactMetaData = registerArtifact(registryClient, artifact,
854 nestedReferences);
855 references.add(buildReferenceFromMetadata(artifactMetaData, artifact.getName()));
856 }
857 return references;
858 }
859
860 public void setArtifacts(List<RegisterArtifact> artifacts) {
861 this.artifacts = artifacts;
862 }
863
864 @Override
865 public void setRegistryUrl(String registryUrl) {
866 super.setRegistryUrl(registryUrl);
867 this.registryArtifactUrlPattern = registryUrl == null
868 ? null
869 : ReferenceUrlUtil.createRegistryArtifactUrlPattern(registryUrl);
870 }
871
872 public void setSkip(boolean skip) {
873 this.skip = skip;
874 }
875
876 private static ArtifactReference buildReferenceFromMetadata(VersionMetaData metaData,
877 String referenceName) {
878 ArtifactReference reference = new ArtifactReference();
879 reference.setName(referenceName);
880 reference.setArtifactId(metaData.getArtifactId());
881 reference.setGroupId(metaData.getGroupId());
882 reference.setVersion(metaData.getVersion());
883 return reference;
884 }
885
886 private static boolean isFileAllowedInIndex(File file) {
887 return file.isFile() && (
888 file.getName().toLowerCase().endsWith(".json") ||
889 file.getName().toLowerCase().endsWith(".yml") ||
890 file.getName().toLowerCase().endsWith(".yaml") ||
891 file.getName().toLowerCase().endsWith(".xml") ||
892 file.getName().toLowerCase().endsWith(".xsd") ||
893 file.getName().toLowerCase().endsWith(".wsdl") ||
894 file.getName().toLowerCase().endsWith(".graphql") ||
895 file.getName().toLowerCase().endsWith(".avsc") ||
896 file.getName().toLowerCase().endsWith(".proto")
897 );
898 }
899
900
901
902
903
904
905 private static ReferenceIndex createIndex(RegisterArtifact artifact) {
906 File file = artifact.getFile();
907 ReferenceIndex index = new ReferenceIndex(file.getParentFile().toPath());
908 if (artifact.getProtoPaths() != null) {
909 artifact.getProtoPaths().forEach(path -> index.addSchemaPath(path.toPath()));
910 }
911
912 HashSet<File> roots = new HashSet<>();
913 if (artifact.getProtoPaths() != null) {
914 roots.addAll(artifact.getProtoPaths());
915 } else {
916 roots.add(file.getParentFile());
917 }
918
919 Collection<File> allFiles = new HashSet<>();
920 for (File root : roots) {
921 allFiles.addAll(FileUtils.listFiles(root, null, true));
922 allFiles.stream().filter(RegisterRegistryMojo::isFileAllowedInIndex).forEach(f -> {
923 index.index(f.toPath(), readContent(f));
924 });
925 }
926
927 return index;
928 }
929
930 private void addExistingReferencesToIndex(RegistryClient registryClient, ReferenceIndex index,
931 List<ExistingReference> existingReferences) throws ExecutionException, InterruptedException {
932 if (existingReferences != null && !existingReferences.isEmpty()) {
933 for (ExistingReference ref : existingReferences) {
934 VersionMetaData vmd;
935 if (ref.getVersion() == null || "LATEST".equalsIgnoreCase(ref.getVersion())) {
936 vmd = registryClient.groups().byGroupId(ref.getGroupId()).artifacts()
937 .byArtifactId(ref.getArtifactId()).versions().byVersionExpression("branch=latest")
938 .get();
939 } else {
940 vmd = new VersionMetaData();
941 vmd.setGroupId(ref.getGroupId());
942 vmd.setArtifactId(ref.getArtifactId());
943 vmd.setVersion(ref.getVersion());
944 }
945 index.index(ref.getResourceName(), vmd);
946 }
947 }
948 }
949
950 protected static ContentHandle readContent(File file) {
951 try {
952 return ContentHandle.create(Files.readAllBytes(file.toPath()));
953 } catch (IOException e) {
954 throw new RuntimeException("Failed to read schema file: " + file, e);
955 }
956 }
957
958 protected static RegisterArtifact buildFromRoot(RegisterArtifact rootArtifact, String artifactId, String groupId) {
959 RegisterArtifact nestedSchema = new RegisterArtifact();
960 nestedSchema.setCanonicalize(rootArtifact.getCanonicalize());
961 nestedSchema.setArtifactId(artifactId);
962 nestedSchema.setGroupId(groupId == null ? rootArtifact.getGroupId() : groupId);
963 nestedSchema.setContentType(rootArtifact.getContentType());
964 nestedSchema.setArtifactType(rootArtifact.getArtifactType());
965 nestedSchema.setMinify(rootArtifact.getMinify());
966 nestedSchema.setContentType(rootArtifact.getContentType());
967 nestedSchema.setIfExists(rootArtifact.getIfExists());
968 nestedSchema.setAutoRefs(rootArtifact.getAutoRefs());
969 return nestedSchema;
970 }
971
972 private static File getLocalFile(Path path) {
973 return path.toFile();
974 }
975
976
977
978
979
980
981
982 private static boolean loopDetected(RegisterArtifact artifact,
983 Stack<RegisterArtifact> registrationStack) {
984 for (RegisterArtifact stackArtifact : registrationStack) {
985 if (artifact.getFile().equals(stackArtifact.getFile())) {
986 return true;
987 }
988 }
989 return false;
990 }
991
992 private static String printLoop(Stack<RegisterArtifact> registrationStack) {
993 return registrationStack.stream().map(artifact -> artifact.getFile().getName())
994 .collect(Collectors.joining(" -> "));
995 }
996
997 }