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 IOException, ExecutionException,
623             InterruptedException, MojoExecutionException, MojoFailureException {
624         if (artifact.getFile() != null) {
625             try (InputStream artifactContent = new FileInputStream(artifact.getFile())) {
626                 return registerArtifact(registryClient, artifact, artifactContent, references);
627             }
628         } else {
629             return getArtifactVersionMetadata(registryClient, artifact);
630         }
631     }
632 
633     private VersionMetaData getArtifactVersionMetadata(RegistryClient registryClient,
634                                                        RegisterArtifact artifact) {
635         String groupId = artifact.getGroupId();
636         String artifactId = artifact.getArtifactId();
637         String version = artifact.getVersion();
638 
639         VersionMetaData amd = registryClient.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId)
640                 .versions().byVersionExpression(version).get();
641         getLog().info(String.format("Successfully processed artifact [%s] / [%s].  GlobalId is [%d]", groupId,
642                 artifactId, amd.getGlobalId()));
643 
644         return amd;
645     }
646 
647     private VersionMetaData registerArtifact(RegistryClient registryClient, RegisterArtifact artifact,
648                                              InputStream artifactContent, List<ArtifactReference> references)
649             throws ExecutionException, InterruptedException, MojoFailureException, MojoExecutionException {
650         String groupId = artifact.getGroupId();
651         String artifactId = artifact.getArtifactId();
652         String type = artifact.getArtifactType();
653         Boolean canonicalize = artifact.getCanonicalize();
654         String ct = artifact.getContentType() == null ? ContentTypes.APPLICATION_JSON
655                 : artifact.getContentType();
656         String data = null;
657         try {
658             if (artifact.getMinify() != null && artifact.getMinify()) {
659                 ObjectMapper objectMapper = new ObjectMapper();
660                 JsonNode jsonNode = objectMapper.readValue(artifactContent, JsonNode.class);
661                 data = jsonNode.toString();
662             } else {
663                 data = new String(artifactContent.readAllBytes(), StandardCharsets.UTF_8);
664             }
665         } catch (IOException e) {
666             throw new RuntimeException(e);
667         }
668         ResolvedArtifactVersion resolvedArtifactVersion = resolveVersion(artifact, data);
669 
670         CreateArtifact createArtifact = new CreateArtifact();
671         createArtifact.setArtifactId(artifactId);
672         createArtifact.setArtifactType(type);
673 
674         CreateVersion createVersion = new CreateVersion();
675         createVersion.setVersion(resolvedArtifactVersion.version());
676         createVersion.setIsDraft(resolvedArtifactVersion.isDraft());
677         createArtifact.setFirstVersion(createVersion);
678 
679         VersionContent content = new VersionContent();
680         content.setContent(data);
681         content.setContentType(ct);
682         content.setReferences(references.stream().map(r -> {
683             ArtifactReference ref = new ArtifactReference();
684             ref.setArtifactId(r.getArtifactId());
685             ref.setGroupId(r.getGroupId());
686             ref.setVersion(r.getVersion());
687             ref.setName(r.getName());
688             return ref;
689         }).collect(Collectors.toList()));
690         createVersion.setContent(content);
691 
692         try {
693             var vmd = registryClient.groups().byGroupId(groupId).artifacts().post(createArtifact, config -> {
694                 if (artifact.getIfExists() != null) {
695                     config.queryParameters.ifExists = IfArtifactExists
696                             .forValue(artifact.getIfExists().value());
697                     if (dryRun) {
698                         config.queryParameters.dryRun = true;
699                     }
700                 }
701                 config.queryParameters.canonical = canonicalize;
702             });
703 
704             getLog().info(String.format("Successfully registered artifact [%s] / [%s].  GlobalId is [%d]",
705                     groupId, artifactId, vmd.getVersion().getGlobalId()));
706 
707 
708             return vmd.getVersion();
709         } catch (RuleViolationProblemDetails | ProblemDetails e) {
710 
711             if (e.getResponseStatusCode() == 409
712                     && resolvedArtifactVersion.shouldUpdateDraftContentOnConflict()) {
713                 try {
714                     registryClient.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId)
715                             .versions().byVersionExpression(resolvedArtifactVersion.version()).content()
716                             .put(content, config -> {
717 
718                     });
719                     getLog().info(String.format("Successfully updated artifact [%s] / [%s].",
720                             groupId, artifactId));
721                     // Return version metadata
722                     return registryClient.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId)
723                             .versions().byVersionExpression(resolvedArtifactVersion.version()).get();
724                 } catch (RuleViolationProblemDetails | ProblemDetails pd) {
725                     logAndThrow(pd);
726                     return null;
727                 }
728             } else if (e.getResponseStatusCode() == 409
729                     && resolvedArtifactVersion.shouldPromoteDraftOnConflict()) {
730                 return promoteExistingDraftVersion(registryClient, groupId, artifactId, content,
731                         resolvedArtifactVersion, e);
732             } else {
733                 logAndThrow(e);
734                 return null;
735             }
736         }
737     }
738 
739     private ResolvedArtifactVersion resolveVersion(RegisterArtifact artifact, String data) throws MojoExecutionException {
740         if (artifact.getVersion() != null && !artifact.getVersion().isBlank()) {
741             return new ResolvedArtifactVersion(artifact.getVersion(), artifact.getIsDraft(), false, false);
742         }
743 
744         if (artifact.getVersionStrategy() == RegisterArtifact.VersionStrategy.API_INFO_VERSION
745                 && isApiArtifact(artifact.getArtifactType())) {
746             String apiInfoVersion = extractApiInfoVersion(artifact, data);
747             if (apiInfoVersion == null) {
748                 return new ResolvedArtifactVersion(null, artifact.getIsDraft(), false, false);
749             }
750             if (apiInfoVersion.endsWith("-SNAPSHOT")) {
751                 return new ResolvedArtifactVersion(
752                         apiInfoVersion.substring(0, apiInfoVersion.length() - "-SNAPSHOT".length()),
753                         Boolean.TRUE,
754                         true,
755                         true);
756             }
757             return new ResolvedArtifactVersion(apiInfoVersion, artifact.getIsDraft(), true, false);
758         }
759 
760         return new ResolvedArtifactVersion(null, artifact.getIsDraft(), false, false);
761     }
762 
763     private VersionMetaData promoteExistingDraftVersion(RegistryClient registryClient, String groupId,
764                                                         String artifactId, VersionContent content,
765                                                         ResolvedArtifactVersion resolvedArtifactVersion,
766                                                         ApiException registrationError)
767             throws MojoExecutionException, MojoFailureException, InterruptedException, ExecutionException {
768         try {
769             VersionMetaData existingVersion = registryClient.groups().byGroupId(groupId).artifacts()
770                     .byArtifactId(artifactId).versions().byVersionExpression(resolvedArtifactVersion.version()).get();
771             if (!VersionState.DRAFT.equals(existingVersion.getState())) {
772                 logAndThrow(registrationError);
773                 return null;
774             }
775 
776             registryClient.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).versions()
777                     .byVersionExpression(resolvedArtifactVersion.version()).content().put(content, config -> {
778                     });
779 
780             WrappedVersionState enabled = new WrappedVersionState();
781             enabled.setState(VersionState.ENABLED);
782             registryClient.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).versions()
783                     .byVersionExpression(resolvedArtifactVersion.version()).state().put(enabled);
784 
785             getLog().info(String.format("Successfully promoted draft artifact [%s] / [%s] version [%s].",
786                     groupId, artifactId, resolvedArtifactVersion.version()));
787 
788             return registryClient.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).versions()
789                     .byVersionExpression(resolvedArtifactVersion.version()).get();
790         } catch (RuleViolationProblemDetails | ProblemDetails e) {
791             logAndThrow(e);
792             return null;
793         }
794     }
795 
796     private boolean isApiArtifact(String artifactType) {
797         return ArtifactType.OPENAPI.equals(artifactType) || ArtifactType.ASYNCAPI.equals(artifactType);
798     }
799 
800     private String extractApiInfoVersion(RegisterArtifact artifact, String data) throws MojoExecutionException {
801         try {
802             JsonNode root = getVersionExtractionMapper(artifact).readTree(data);
803             JsonNode versionNode = root.path("info").path("version");
804             if (versionNode.isTextual()) {
805                 String version = versionNode.asText();
806                 if (!version.isBlank()) {
807                     return version;
808                 }
809             }
810             return null;
811         } catch (IOException e) {
812             throw new MojoExecutionException("Failed to extract info.version from API artifact: "
813                     + artifact.getFile().getPath(), e);
814         }
815     }
816 
817     private ObjectMapper getVersionExtractionMapper(RegisterArtifact artifact) {
818         return isYamlContent(artifact) ? new ObjectMapper(new YAMLFactory()) : new ObjectMapper();
819     }
820 
821     private boolean isYamlContent(RegisterArtifact artifact) {
822         String contentType = artifact.getContentType();
823         if (contentType != null) {
824             String normalizedContentType = contentType.toLowerCase(Locale.ROOT);
825             if (normalizedContentType.contains("yaml") || normalizedContentType.contains("yml")) {
826                 return true;
827             }
828         }
829 
830         File file = artifact.getFile();
831         if (file == null) {
832             return false;
833         }
834 
835         String fileName = file.getName().toLowerCase(Locale.ROOT);
836         return fileName.endsWith(".yaml") || fileName.endsWith(".yml");
837     }
838 
839     private static boolean hasReferences(RegisterArtifact artifact) {
840         return artifact.getReferences() != null && !artifact.getReferences().isEmpty();
841     }
842 
843     private List<ArtifactReference> processArtifactReferences(RegistryClient registryClient,
844                                                               List<RegisterArtifactReference> referencedArtifacts) throws IOException,
845             ExecutionException, InterruptedException, MojoExecutionException, MojoFailureException {
846         List<ArtifactReference> references = new ArrayList<>();
847         for (RegisterArtifactReference artifact : referencedArtifacts) {
848             List<ArtifactReference> nestedReferences = new ArrayList<>();
849             // First, we check if the artifact being processed has references defined, and register them if
850             // needed
851             if (hasReferences(artifact)) {
852                 nestedReferences = processArtifactReferences(registryClient, artifact.getReferences());
853             }
854             final VersionMetaData artifactMetaData = registerArtifact(registryClient, artifact,
855                     nestedReferences);
856             references.add(buildReferenceFromMetadata(artifactMetaData, artifact.getName()));
857         }
858         return references;
859     }
860 
861     public void setArtifacts(List<RegisterArtifact> artifacts) {
862         this.artifacts = artifacts;
863     }
864 
865     @Override
866     public void setRegistryUrl(String registryUrl) {
867         super.setRegistryUrl(registryUrl);
868         this.registryArtifactUrlPattern = registryUrl == null
869                 ? null
870                 : ReferenceUrlUtil.createRegistryArtifactUrlPattern(registryUrl);
871     }
872 
873     public void setSkip(boolean skip) {
874         this.skip = skip;
875     }
876 
877     private static ArtifactReference buildReferenceFromMetadata(VersionMetaData metaData,
878                                                                 String referenceName) {
879         ArtifactReference reference = new ArtifactReference();
880         reference.setName(referenceName);
881         reference.setArtifactId(metaData.getArtifactId());
882         reference.setGroupId(metaData.getGroupId());
883         reference.setVersion(metaData.getVersion());
884         return reference;
885     }
886 
887     private static boolean isFileAllowedInIndex(File file) {
888         return file.isFile() && (
889                 file.getName().toLowerCase().endsWith(".json") ||
890                         file.getName().toLowerCase().endsWith(".yml") ||
891                         file.getName().toLowerCase().endsWith(".yaml") ||
892                         file.getName().toLowerCase().endsWith(".xml") ||
893                         file.getName().toLowerCase().endsWith(".xsd") ||
894                         file.getName().toLowerCase().endsWith(".wsdl") ||
895                         file.getName().toLowerCase().endsWith(".graphql") ||
896                         file.getName().toLowerCase().endsWith(".avsc") ||
897                         file.getName().toLowerCase().endsWith(".proto")
898         );
899     }
900 
901     /**
902      * Create a local index relative to the given file location.
903      *
904      * @param artifact
905      */
906     private static ReferenceIndex createIndex(RegisterArtifact artifact) {
907         File file = artifact.getFile();
908         ReferenceIndex index = new ReferenceIndex(file.getParentFile().toPath());
909         if (artifact.getProtoPaths() != null) {
910             artifact.getProtoPaths().forEach(path -> index.addSchemaPath(path.toPath()));
911         }
912 
913         HashSet<File> roots = new HashSet<>();
914         if (artifact.getProtoPaths() != null) {
915             roots.addAll(artifact.getProtoPaths());
916         } else {
917             roots.add(file.getParentFile());
918         }
919 
920         Collection<File> allFiles = new HashSet<>();
921         for (File root : roots) {
922             allFiles.addAll(FileUtils.listFiles(root, null, true));
923             allFiles.stream().filter(RegisterRegistryMojo::isFileAllowedInIndex).forEach(f -> {
924                 index.index(f.toPath(), readContent(f));
925             });
926         }
927 
928         return index;
929     }
930 
931     private void addExistingReferencesToIndex(RegistryClient registryClient, ReferenceIndex index,
932                                               List<ExistingReference> existingReferences) throws ExecutionException, InterruptedException {
933         if (existingReferences != null && !existingReferences.isEmpty()) {
934             for (ExistingReference ref : existingReferences) {
935                 VersionMetaData vmd;
936                 if (ref.getVersion() == null || "LATEST".equalsIgnoreCase(ref.getVersion())) {
937                     vmd = registryClient.groups().byGroupId(ref.getGroupId()).artifacts()
938                             .byArtifactId(ref.getArtifactId()).versions().byVersionExpression("branch=latest")
939                             .get();
940                 } else {
941                     vmd = new VersionMetaData();
942                     vmd.setGroupId(ref.getGroupId());
943                     vmd.setArtifactId(ref.getArtifactId());
944                     vmd.setVersion(ref.getVersion());
945                 }
946                 index.index(ref.getResourceName(), vmd);
947             }
948         }
949     }
950 
951     protected static ContentHandle readContent(File file) {
952         try {
953             return ContentHandle.create(Files.readAllBytes(file.toPath()));
954         } catch (IOException e) {
955             throw new RuntimeException("Failed to read schema file: " + file, e);
956         }
957     }
958 
959     protected static RegisterArtifact buildFromRoot(RegisterArtifact rootArtifact, String artifactId, String groupId) {
960         RegisterArtifact nestedSchema = new RegisterArtifact();
961         nestedSchema.setCanonicalize(rootArtifact.getCanonicalize());
962         nestedSchema.setArtifactId(artifactId);
963         nestedSchema.setGroupId(groupId == null ? rootArtifact.getGroupId() : groupId);
964         nestedSchema.setContentType(rootArtifact.getContentType());
965         nestedSchema.setArtifactType(rootArtifact.getArtifactType());
966         nestedSchema.setMinify(rootArtifact.getMinify());
967         nestedSchema.setContentType(rootArtifact.getContentType());
968         nestedSchema.setIfExists(rootArtifact.getIfExists());
969         nestedSchema.setAutoRefs(rootArtifact.getAutoRefs());
970         return nestedSchema;
971     }
972 
973     private static File getLocalFile(Path path) {
974         return path.toFile();
975     }
976 
977     /**
978      * Detects a loop by looking for the given artifact in the registration stack.
979      *
980      * @param artifact
981      * @param registrationStack
982      */
983     private static boolean loopDetected(RegisterArtifact artifact,
984                                         Stack<RegisterArtifact> registrationStack) {
985         for (RegisterArtifact stackArtifact : registrationStack) {
986             if (artifact.getFile().equals(stackArtifact.getFile())) {
987                 return true;
988             }
989         }
990         return false;
991     }
992 
993     private static String printLoop(Stack<RegisterArtifact> registrationStack) {
994         return registrationStack.stream().map(artifact -> artifact.getFile().getName())
995                 .collect(Collectors.joining(" -> "));
996     }
997 
998 }