1 package io.apicurio.registry.maven;
2
3 import com.fasterxml.jackson.databind.JsonNode;
4 import com.fasterxml.jackson.databind.ObjectMapper;
5 import com.google.protobuf.Descriptors.FileDescriptor;
6 import io.apicurio.registry.content.ContentHandle;
7 import io.apicurio.registry.content.TypedContent;
8 import io.apicurio.registry.content.refs.ExternalReference;
9 import io.apicurio.registry.content.refs.ReferenceFinder;
10 import io.apicurio.registry.maven.refs.IndexedResource;
11 import io.apicurio.registry.maven.refs.ReferenceIndex;
12 import io.apicurio.registry.rest.client.RegistryClient;
13 import io.apicurio.registry.rest.client.models.*;
14 import io.apicurio.registry.rules.ParsedJsonSchema;
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.avro.Schema;
21 import org.apache.commons.io.FileUtils;
22 import org.apache.maven.plugin.MojoExecutionException;
23 import org.apache.maven.plugin.MojoFailureException;
24 import org.apache.maven.plugins.annotations.Mojo;
25 import org.apache.maven.plugins.annotations.Parameter;
26
27 import java.io.*;
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
36
37
38
39 @Mojo(name = "register")
40 public class RegisterRegistryMojo extends AbstractRegistryMojo {
41
42
43
44
45 @Parameter(required = false)
46 List<ExistingReference> existingReferences;
47
48
49
50
51 @Parameter(required = true)
52 List<RegisterArtifact> artifacts;
53
54
55
56
57 @Parameter(property = "skipRegister", defaultValue = "false")
58 boolean skip;
59
60
61
62
63
64 @Parameter(property = "dryRun", defaultValue = "false")
65 boolean dryRun;
66
67 DefaultArtifactTypeUtilProviderImpl utilProviderFactory = new DefaultArtifactTypeUtilProviderImpl();
68
69
70
71
72 protected boolean validate() throws MojoExecutionException {
73 if (skip) {
74 getLog().info("register is skipped.");
75 return false;
76 }
77
78 if (artifacts == null || artifacts.isEmpty()) {
79 getLog().warn("No artifacts are configured for registration.");
80 return false;
81 }
82
83 if (existingReferences == null) {
84 existingReferences = new ArrayList<>();
85 }
86
87 int idx = 0;
88 int errorCount = 0;
89 for (RegisterArtifact artifact : artifacts) {
90 if (artifact.getGroupId() == null) {
91 getLog().error(String.format(
92 "GroupId is required when registering an artifact. Missing from artifacts[%d].",
93 idx));
94 errorCount++;
95 }
96 if (artifact.getArtifactId() == null) {
97 getLog().error(String.format(
98 "ArtifactId is required when registering an artifact. Missing from artifacts[%s].",
99 idx));
100 errorCount++;
101 }
102 if (artifact.getFile() == null) {
103 getLog().error(String.format(
104 "File is required when registering an artifact. Missing from artifacts[%s].", idx));
105 errorCount++;
106 } else if (!artifact.getFile().exists()) {
107 getLog().error(
108 String.format("Artifact file to register is configured but file does not exist: %s",
109 artifact.getFile().getPath()));
110 errorCount++;
111 }
112
113 idx++;
114 }
115
116 if (errorCount > 0) {
117 throw new MojoExecutionException(
118 "Invalid configuration of the Register Artifact(s) mojo. See the output log for details.");
119 }
120 return true;
121 }
122
123 @Override
124 protected void executeInternal() throws MojoExecutionException {
125 int errorCount = 0;
126 if (validate()) {
127 Vertx vertx = createVertx();
128 RegistryClient registryClient = createClient(vertx);
129
130 for (RegisterArtifact artifact : artifacts) {
131 String groupId = artifact.getGroupId();
132 String artifactId = artifact.getArtifactId();
133 try {
134 if (artifact.getAutoRefs() != null && artifact.getAutoRefs()) {
135
136
137 ReferenceIndex index = createIndex(artifact.getFile());
138 addExistingReferencesToIndex(registryClient, index, existingReferences);
139 addExistingReferencesToIndex(registryClient, index, artifact.getExistingReferences());
140 Stack<RegisterArtifact> registrationStack = new Stack<>();
141
142 registerWithAutoRefs(registryClient, artifact, index, registrationStack);
143 } else if (artifact.getAnalyzeDirectory() != null && artifact.getAnalyzeDirectory()) {
144
145
146 getLog().warn("Deprecated feature in use: 'analyzeDirectory' -- use 'autoRefs' instead.");
147 registerDirectory(registryClient, artifact);
148 } else {
149
150 List<ArtifactReference> references = new ArrayList<>();
151
152 if (hasReferences(artifact)) {
153 references = processArtifactReferences(registryClient, artifact.getReferences());
154 }
155 registerArtifact(registryClient, artifact, references);
156 }
157 } catch (Exception e) {
158 errorCount++;
159 getLog().error(String.format("Exception while registering artifact [%s] / [%s]", groupId,
160 artifactId), e);
161 }
162
163 }
164
165 if (errorCount > 0) {
166 throw new MojoExecutionException("Errors while registering artifacts ...");
167 }
168 }
169 }
170
171 private VersionMetaData registerWithAutoRefs(RegistryClient registryClient, RegisterArtifact artifact,
172 ReferenceIndex index, Stack<RegisterArtifact> registrationStack) throws IOException,
173 ExecutionException, InterruptedException, MojoExecutionException, MojoFailureException {
174 if (loopDetected(artifact, registrationStack)) {
175 throw new RuntimeException(
176 "Artifact reference loop detected (not supported): " + printLoop(registrationStack));
177 }
178 registrationStack.push(artifact);
179
180
181 ContentHandle artifactContent = readContent(artifact.getFile());
182 String artifactContentType = getContentTypeByExtension(artifact.getFile().getName());
183 TypedContent typedArtifactContent = TypedContent.create(artifactContent, artifactContentType);
184
185
186 ArtifactTypeUtilProvider provider = this.utilProviderFactory
187 .getArtifactTypeProvider(artifact.getArtifactType());
188 ReferenceFinder referenceFinder = provider.getReferenceFinder();
189 var referenceArtifactIdentifierExtractor = provider.getReferenceArtifactIdentifierExtractor();
190 Set<ExternalReference> externalReferences = referenceFinder
191 .findExternalReferences(typedArtifactContent);
192
193
194 List<ArtifactReference> registeredReferences = new ArrayList<>(externalReferences.size());
195 for (ExternalReference externalRef : externalReferences) {
196 IndexedResource iresource = index.lookup(externalRef.getResource(),
197 Paths.get(artifact.getFile().toURI()));
198
199
200 if (iresource == null) {
201 throw new RuntimeException("Reference could not be resolved. From: "
202 + artifact.getFile().getName() + " To: " + externalRef.getFullReference());
203 }
204
205
206 if (!iresource.isRegistered()) {
207
208 String artifactId = referenceArtifactIdentifierExtractor.extractArtifactId(externalRef.getResource());
209 String groupId = referenceArtifactIdentifierExtractor.extractGroupId(externalRef.getResource());
210 File localFile = getLocalFile(iresource.getPath());
211 RegisterArtifact refArtifact = buildFromRoot(artifact, artifactId, groupId);
212 refArtifact.setArtifactType(iresource.getType());
213 refArtifact.setVersion(null);
214 refArtifact.setFile(localFile);
215 refArtifact.setContentType(getContentTypeByExtension(localFile.getName()));
216 try {
217 var car = registerWithAutoRefs(registryClient, refArtifact, index, registrationStack);
218 iresource.setRegistration(car);
219 } catch (IOException | ExecutionException | InterruptedException e) {
220 throw new RuntimeException(e);
221 }
222 }
223
224 var reference = new ArtifactReference();
225 reference.setName(externalRef.getFullReference());
226 reference.setVersion(iresource.getRegistration().getVersion());
227 reference.setGroupId(iresource.getRegistration().getGroupId());
228 reference.setArtifactId(iresource.getRegistration().getArtifactId());
229 registeredReferences.add(reference);
230 }
231 registeredReferences.sort((ref1, ref2) -> ref1.getName().compareTo(ref2.getName()));
232
233 registrationStack.pop();
234 return registerArtifact(registryClient, artifact, registeredReferences);
235 }
236
237 private void registerDirectory(RegistryClient registryClient, RegisterArtifact artifact)
238 throws IOException, ExecutionException, InterruptedException, MojoExecutionException,
239 MojoFailureException {
240 switch (artifact.getArtifactType()) {
241 case ArtifactType.AVRO:
242 final AvroDirectoryParser avroDirectoryParser = new AvroDirectoryParser(registryClient);
243 final ParsedDirectoryWrapper<Schema> schema = avroDirectoryParser.parse(artifact.getFile());
244 registerArtifact(registryClient, artifact, avroDirectoryParser
245 .handleSchemaReferences(artifact, schema.getSchema(), schema.getSchemaContents()));
246 break;
247 case ArtifactType.PROTOBUF:
248 final ProtobufDirectoryParser protobufDirectoryParser = new ProtobufDirectoryParser(
249 registryClient);
250 final ParsedDirectoryWrapper<FileDescriptor> protoSchema = protobufDirectoryParser
251 .parse(artifact.getFile());
252 registerArtifact(registryClient, artifact, protobufDirectoryParser.handleSchemaReferences(
253 artifact, protoSchema.getSchema(), protoSchema.getSchemaContents()));
254 break;
255 case ArtifactType.JSON:
256 final JsonSchemaDirectoryParser jsonSchemaDirectoryParser = new JsonSchemaDirectoryParser(
257 registryClient);
258 final ParsedDirectoryWrapper<ParsedJsonSchema> jsonSchema = jsonSchemaDirectoryParser
259 .parse(artifact.getFile());
260 registerArtifact(registryClient, artifact, jsonSchemaDirectoryParser.handleSchemaReferences(
261 artifact, jsonSchema.getSchema(), jsonSchema.getSchemaContents()));
262 break;
263 default:
264 throw new IllegalArgumentException(
265 String.format("Artifact type not recognized for analyzing a directory structure %s",
266 artifact.getArtifactType()));
267 }
268 }
269
270 private VersionMetaData registerArtifact(RegistryClient registryClient, RegisterArtifact artifact,
271 List<ArtifactReference> references) throws FileNotFoundException, ExecutionException,
272 InterruptedException, MojoExecutionException, MojoFailureException {
273 if (artifact.getFile() != null) {
274 return registerArtifact(registryClient, artifact, new FileInputStream(artifact.getFile()),
275 references);
276 } else {
277 return getArtifactVersionMetadata(registryClient, artifact);
278 }
279 }
280
281 private VersionMetaData getArtifactVersionMetadata(RegistryClient registryClient,
282 RegisterArtifact artifact) {
283 String groupId = artifact.getGroupId();
284 String artifactId = artifact.getArtifactId();
285 String version = artifact.getVersion();
286
287 VersionMetaData amd = registryClient.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId)
288 .versions().byVersionExpression(version).get();
289 getLog().info(String.format("Successfully processed artifact [%s] / [%s]. GlobalId is [%d]", groupId,
290 artifactId, amd.getGlobalId()));
291
292 return amd;
293 }
294
295 private VersionMetaData registerArtifact(RegistryClient registryClient, RegisterArtifact artifact,
296 InputStream artifactContent, List<ArtifactReference> references)
297 throws ExecutionException, InterruptedException, MojoFailureException, MojoExecutionException {
298 String groupId = artifact.getGroupId();
299 String artifactId = artifact.getArtifactId();
300 String version = artifact.getVersion();
301 String type = artifact.getArtifactType();
302 Boolean canonicalize = artifact.getCanonicalize();
303 Boolean isDraft = artifact.getIsDraft();
304 String ct = artifact.getContentType() == null ? ContentTypes.APPLICATION_JSON
305 : artifact.getContentType();
306 String data = null;
307 try {
308 if (artifact.getMinify() != null && artifact.getMinify()) {
309 ObjectMapper objectMapper = new ObjectMapper();
310 JsonNode jsonNode = objectMapper.readValue(artifactContent, JsonNode.class);
311 data = jsonNode.toString();
312 } else {
313 data = new String(artifactContent.readAllBytes(), StandardCharsets.UTF_8);
314 }
315 } catch (IOException e) {
316 throw new RuntimeException(e);
317 }
318
319 CreateArtifact createArtifact = new CreateArtifact();
320 createArtifact.setArtifactId(artifactId);
321 createArtifact.setArtifactType(type);
322
323 CreateVersion createVersion = new CreateVersion();
324 createVersion.setVersion(version);
325 createVersion.setIsDraft(isDraft);
326 createArtifact.setFirstVersion(createVersion);
327
328 VersionContent content = new VersionContent();
329 content.setContent(data);
330 content.setContentType(ct);
331 content.setReferences(references.stream().map(r -> {
332 ArtifactReference ref = new ArtifactReference();
333 ref.setArtifactId(r.getArtifactId());
334 ref.setGroupId(r.getGroupId());
335 ref.setVersion(r.getVersion());
336 ref.setName(r.getName());
337 return ref;
338 }).collect(Collectors.toList()));
339 createVersion.setContent(content);
340
341 try {
342 var vmd = registryClient.groups().byGroupId(groupId).artifacts().post(createArtifact, config -> {
343 if (artifact.getIfExists() != null) {
344 config.queryParameters.ifExists = IfArtifactExists
345 .forValue(artifact.getIfExists().value());
346 if (dryRun) {
347 config.queryParameters.dryRun = true;
348 }
349 }
350 config.queryParameters.canonical = canonicalize;
351 });
352
353 getLog().info(String.format("Successfully registered artifact [%s] / [%s]. GlobalId is [%d]",
354 groupId, artifactId, vmd.getVersion().getGlobalId()));
355
356
357 return vmd.getVersion();
358 } catch (RuleViolationProblemDetails | ProblemDetails e) {
359
360
361 if (artifact.getIsDraft() && e.getResponseStatusCode() == 409) {
362 try {
363 registryClient.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId)
364 .versions().byVersionExpression(version).content()
365 .put(content, config -> {
366
367 });
368 getLog().info(String.format("Successfully updated artifact [%s] / [%s].",
369 groupId, artifactId));
370
371 return registryClient.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).versions().byVersionExpression(version).get();
372 } catch (RuleViolationProblemDetails | ProblemDetails pd) {
373 logAndThrow(pd);
374 return null;
375 }
376 } else {
377 logAndThrow(e);
378 return null;
379 }
380 }
381 }
382
383 private static boolean hasReferences(RegisterArtifact artifact) {
384 return artifact.getReferences() != null && !artifact.getReferences().isEmpty();
385 }
386
387 private List<ArtifactReference> processArtifactReferences(RegistryClient registryClient,
388 List<RegisterArtifactReference> referencedArtifacts) throws FileNotFoundException,
389 ExecutionException, InterruptedException, MojoExecutionException, MojoFailureException {
390 List<ArtifactReference> references = new ArrayList<>();
391 for (RegisterArtifactReference artifact : referencedArtifacts) {
392 List<ArtifactReference> nestedReferences = new ArrayList<>();
393
394
395 if (hasReferences(artifact)) {
396 nestedReferences = processArtifactReferences(registryClient, artifact.getReferences());
397 }
398 final VersionMetaData artifactMetaData = registerArtifact(registryClient, artifact,
399 nestedReferences);
400 references.add(buildReferenceFromMetadata(artifactMetaData, artifact.getName()));
401 }
402 return references;
403 }
404
405 public void setArtifacts(List<RegisterArtifact> artifacts) {
406 this.artifacts = artifacts;
407 }
408
409 public void setSkip(boolean skip) {
410 this.skip = skip;
411 }
412
413 private static ArtifactReference buildReferenceFromMetadata(VersionMetaData metaData,
414 String referenceName) {
415 ArtifactReference reference = new ArtifactReference();
416 reference.setName(referenceName);
417 reference.setArtifactId(metaData.getArtifactId());
418 reference.setGroupId(metaData.getGroupId());
419 reference.setVersion(metaData.getVersion());
420 return reference;
421 }
422
423 private static boolean isFileAllowedInIndex(File file) {
424 return file.isFile() && (
425 file.getName().toLowerCase().endsWith(".json") ||
426 file.getName().toLowerCase().endsWith(".yml") ||
427 file.getName().toLowerCase().endsWith(".yaml") ||
428 file.getName().toLowerCase().endsWith(".xml") ||
429 file.getName().toLowerCase().endsWith(".xsd") ||
430 file.getName().toLowerCase().endsWith(".wsdl") ||
431 file.getName().toLowerCase().endsWith(".graphql") ||
432 file.getName().toLowerCase().endsWith(".avsc") ||
433 file.getName().toLowerCase().endsWith(".proto")
434 );
435 }
436
437
438
439
440
441
442 private static ReferenceIndex createIndex(File file) {
443 ReferenceIndex index = new ReferenceIndex(file.getParentFile().toPath());
444 Collection<File> allFiles = FileUtils.listFiles(file.getParentFile(), null, true);
445 allFiles.stream().filter(RegisterRegistryMojo::isFileAllowedInIndex).forEach(f -> {
446 index.index(f.toPath(), readContent(f));
447 });
448 return index;
449 }
450
451 private void addExistingReferencesToIndex(RegistryClient registryClient, ReferenceIndex index,
452 List<ExistingReference> existingReferences) throws ExecutionException, InterruptedException {
453 if (existingReferences != null && !existingReferences.isEmpty()) {
454 for (ExistingReference ref : existingReferences) {
455 VersionMetaData vmd;
456 if (ref.getVersion() == null || "LATEST".equalsIgnoreCase(ref.getVersion())) {
457 vmd = registryClient.groups().byGroupId(ref.getGroupId()).artifacts()
458 .byArtifactId(ref.getArtifactId()).versions().byVersionExpression("branch=latest")
459 .get();
460 } else {
461 vmd = new VersionMetaData();
462 vmd.setGroupId(ref.getGroupId());
463 vmd.setArtifactId(ref.getArtifactId());
464 vmd.setVersion(ref.getVersion());
465 }
466 index.index(ref.getResourceName(), vmd);
467 }
468 }
469 }
470
471 protected static ContentHandle readContent(File file) {
472 try {
473 return ContentHandle.create(Files.readAllBytes(file.toPath()));
474 } catch (IOException e) {
475 throw new RuntimeException("Failed to read schema file: " + file, e);
476 }
477 }
478
479 protected static RegisterArtifact buildFromRoot(RegisterArtifact rootArtifact, String artifactId, String groupId) {
480 RegisterArtifact nestedSchema = new RegisterArtifact();
481 nestedSchema.setCanonicalize(rootArtifact.getCanonicalize());
482 nestedSchema.setArtifactId(artifactId);
483 nestedSchema.setGroupId(groupId == null ? rootArtifact.getGroupId() : groupId);
484 nestedSchema.setContentType(rootArtifact.getContentType());
485 nestedSchema.setArtifactType(rootArtifact.getArtifactType());
486 nestedSchema.setMinify(rootArtifact.getMinify());
487 nestedSchema.setContentType(rootArtifact.getContentType());
488 nestedSchema.setIfExists(rootArtifact.getIfExists());
489 nestedSchema.setAutoRefs(rootArtifact.getAutoRefs());
490 return nestedSchema;
491 }
492
493 private static File getLocalFile(Path path) {
494 return path.toFile();
495 }
496
497
498
499
500
501
502
503 private static boolean loopDetected(RegisterArtifact artifact,
504 Stack<RegisterArtifact> registrationStack) {
505 for (RegisterArtifact stackArtifact : registrationStack) {
506 if (artifact.getFile().equals(stackArtifact.getFile())) {
507 return true;
508 }
509 }
510 return false;
511 }
512
513 private static String printLoop(Stack<RegisterArtifact> registrationStack) {
514 return registrationStack.stream().map(artifact -> artifact.getFile().getName())
515 .collect(Collectors.joining(" -> "));
516 }
517
518 }