View Javadoc
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   * Register artifacts against registry.
40   */
41  @Mojo(name = "register", requiresProject = false)
42  public class RegisterRegistryMojo extends AbstractRegistryMojo {
43  
44      /**
45       * The list of pre-registered artifacts that can be used as references.
46       */
47      @Parameter(required = false)
48      List<ExistingReference> existingReferences;
49  
50      /**
51       * The list of artifacts to register.
52       */
53      @Parameter(required = false)
54      List<RegisterArtifact> artifacts;
55  
56      /**
57       * Set this to 'true' to skip registering the artifact(s). Convenient in case you want to skip for specific occasions.
58       */
59      @Parameter(property = "skipRegister", defaultValue = "false")
60      boolean skip;
61  
62      /**
63       * Set this to 'true' to perform the action with the "dryRun" option enabled. This will effectively test
64       * whether registration *would have worked*. But it results in no changes made on the server.
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       * Validate the configuration.
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                         // If we have references, then we'll need to create the local resource index and then
441                         // process all refs.
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                         // First, we check if the artifact being processed has references defined
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         // Read the artifact content.
481         ContentHandle artifactContent = readContent(artifact.getFile());
482         String artifactContentType = getContentTypeByExtension(artifact.getFile().getName());
483         // Set the content type on the artifact if not already explicitly set by the user
484         if (artifact.getContentType() == null) {
485             artifact.setContentType(artifactContentType);
486         }
487         TypedContent typedArtifactContent = TypedContent.create(artifactContent, artifactContentType);
488 
489         // Find all references in the content
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         // Register all the references first, then register the artifact.
498         List<ArtifactReference> registeredReferences = new ArrayList<>(externalReferences.size());
499         Map<String, VersionMetaData> resolvedRegistryReferences = new HashMap<>(); // avoid multiple lookups
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             // If the resource isn't already registered, then register it now.
523             if (!iresource.isRegistered()) {
524                 String groupId = artifact.getGroupId(); // default is same group as root artifact
525                 // TODO: determine the artifactId better (type-specific logic here?)
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(); // same group as root artifact
534                         artifactId = iresource.getResourceName(); // fq name
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                     // Return version metadata
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             // First, we check if the artifact being processed has references defined, and register them if
849             // needed
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      * Create a local index relative to the given file location.
902      *
903      * @param artifact
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      * Detects a loop by looking for the given artifact in the registration stack.
978      *
979      * @param artifact
980      * @param registrationStack
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 }