1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package io.wcm.caravan.maven.plugins.haldocs;
21
22 import io.wcm.caravan.hal.docs.annotations.LinkRelationDoc;
23 import io.wcm.caravan.hal.docs.annotations.ServiceDoc;
24 import io.wcm.caravan.hal.docs.impl.JsonSchemaBundleTracker;
25 import io.wcm.caravan.hal.docs.impl.model.LinkRelation;
26 import io.wcm.caravan.hal.docs.impl.model.Service;
27 import io.wcm.caravan.hal.docs.impl.reader.ServiceJson;
28 import io.wcm.caravan.hal.docs.impl.reader.ServiceModelReader;
29 import io.wcm.caravan.jaxrs.publisher.ApplicationPath;
30
31 import java.io.File;
32 import java.io.FileOutputStream;
33 import java.io.IOException;
34 import java.io.InputStream;
35 import java.io.OutputStream;
36 import java.lang.annotation.Annotation;
37 import java.lang.reflect.Field;
38 import java.net.URLClassLoader;
39 import java.util.Arrays;
40 import java.util.jar.Manifest;
41 import java.util.zip.ZipEntry;
42 import java.util.zip.ZipFile;
43
44 import org.apache.commons.lang3.StringUtils;
45 import org.apache.maven.model.Plugin;
46 import org.apache.maven.plugin.MojoExecutionException;
47 import org.apache.maven.plugin.MojoFailureException;
48 import org.apache.maven.plugins.annotations.LifecyclePhase;
49 import org.apache.maven.plugins.annotations.Mojo;
50 import org.apache.maven.plugins.annotations.Parameter;
51 import org.apache.maven.plugins.annotations.ResolutionScope;
52 import org.codehaus.plexus.util.xml.Xpp3Dom;
53 import org.jsoup.Jsoup;
54
55 import com.thoughtworks.qdox.JavaProjectBuilder;
56 import com.thoughtworks.qdox.model.JavaAnnotatedElement;
57 import com.thoughtworks.qdox.model.JavaClass;
58 import com.thoughtworks.qdox.model.JavaField;
59
60
61
62
63 @Mojo(name = "generate-hal-docs-json", defaultPhase = LifecyclePhase.PROCESS_CLASSES,
64 requiresProject = true, threadSafe = true, requiresDependencyResolution = ResolutionScope.COMPILE)
65 public class GenerateHalDocsJsonMojo extends AbstractBaseMojo {
66
67
68
69
70 @Parameter(defaultValue = "${basedir}/src/main/java")
71 private String source;
72
73
74
75
76
77
78 @Parameter
79 private String serviceId;
80
81
82
83
84 @Parameter(defaultValue = ServiceModelReader.DOCS_CLASSPATH_PREFIX)
85 private String target;
86
87 @Parameter(defaultValue = "generated-hal-docs-resources")
88 private String generatedResourcesDirectory;
89
90 private static final String MAVEN_BUNDLE_PLUGIN_ID = Plugin.constructKey("org.apache.felix", "maven-bundle-plugin");
91
92 @Override
93 public void execute() throws MojoExecutionException, MojoFailureException {
94
95
96 if (StringUtils.isEmpty(serviceId)) {
97 serviceId = getServiceIdFromMavenBundlePlugin();
98 }
99
100
101 if (StringUtils.isEmpty(serviceId)) {
102 getLog().info("No service ID detected, skip HAL documentation file generation.");
103 return;
104 }
105 else {
106 getLog().info("Generate HAL documentation files for service: " + serviceId);
107 }
108
109 try {
110
111 ClassLoader compileClassLoader = URLClassLoader.newInstance(getCompileClasspathElementURLs(), getClass().getClassLoader());
112
113
114 Service service = getServiceInfos(compileClassLoader);
115
116
117 File jsonFile = new File(getGeneratedResourcesDirectory(), ServiceModelReader.SERVICE_DOC_FILE);
118 getLog().info("Write " + jsonFile.getCanonicalPath());
119 try (OutputStream os = new FileOutputStream(jsonFile)) {
120 new ServiceJson().write(service, os);
121 }
122
123
124 addResource(getGeneratedResourcesDirectory().getPath(), target);
125 }
126 catch (Throwable ex) {
127 throw new MojoExecutionException("Generating HAL documentation failed: " + ex.getMessage(), ex);
128 }
129 }
130
131
132
133
134
135 private String getServiceIdFromMavenBundlePlugin() {
136 Plugin bundlePlugin = project.getBuildPlugins().stream()
137 .filter(plugin -> StringUtils.equals(plugin.getKey(), MAVEN_BUNDLE_PLUGIN_ID))
138 .findFirst().orElse(null);
139 if (bundlePlugin != null) {
140 Xpp3Dom configuration = (Xpp3Dom)bundlePlugin.getConfiguration();
141 if (configuration != null) {
142 Xpp3Dom instructions = configuration.getChild("instructions");
143 if (instructions != null) {
144 Xpp3Dom applicationPath = instructions.getChild(ApplicationPath.HEADER_APPLICATON_PATH);
145 if (applicationPath != null) {
146 return applicationPath.getValue();
147 }
148 }
149 }
150 }
151 return null;
152 }
153
154
155
156
157
158 private Service getServiceInfos(ClassLoader compileClassLoader) {
159 Service service = new Service();
160
161
162 service.setServiceId(serviceId);
163 service.setName(project.getName());
164
165
166 JavaProjectBuilder builder = new JavaProjectBuilder();
167 builder.addSourceTree(new File(source));
168 JavaClass serviceInfo = builder.getSources().stream()
169 .flatMap(javaSource -> javaSource.getClasses().stream())
170 .filter(javaClass -> hasAnnotation(javaClass, ServiceDoc.class))
171 .findFirst().orElse(null);
172
173
174 if (serviceInfo != null) {
175 service.setDescriptionMarkup(serviceInfo.getComment());
176
177 serviceInfo.getFields().stream()
178 .filter(field -> hasAnnotation(field, LinkRelationDoc.class))
179 .map(field -> toLinkRelation(serviceInfo, field, compileClassLoader))
180 .forEach(service::addLinkRelation);
181 }
182
183
184 service.resolve();
185
186 return service;
187 }
188
189
190
191
192
193
194
195
196 private LinkRelation toLinkRelation(JavaClass javaClazz, JavaField javaField, ClassLoader compileClassLoader) {
197 LinkRelation rel = new LinkRelation();
198
199 rel.setShortDescription(buildShortDescription(javaField.getComment()));
200 rel.setDescriptionMarkup(javaField.getComment());
201
202 rel.setRel(getStaticFieldValue(javaClazz, javaField, compileClassLoader, String.class));
203
204 LinkRelationDoc relDoc = getAnnotation(javaClazz, javaField, compileClassLoader, LinkRelationDoc.class);
205 rel.setJsonSchemaRef(buildJsonSchemaRefModel(relDoc.jsonSchema(), relDoc.model()));
206
207 Arrays.stream(relDoc.embedded()).forEach(embedded -> rel.addResourceRef(embedded.value(), embedded.description(),
208 buildJsonSchemaRefModel(embedded.jsonSchema(), embedded.model())));
209
210 Arrays.stream(relDoc.links()).forEach(link -> rel.addLinkRelationRef(link.value(), link.description()));
211
212 return rel;
213 }
214
215
216
217
218
219
220
221 private static String buildShortDescription(String descriptionMarkup) {
222 if (StringUtils.isBlank(descriptionMarkup)) {
223 return null;
224 }
225 String text = Jsoup.parse(descriptionMarkup).text();
226 if (StringUtils.isBlank(text)) {
227 return null;
228 }
229 if (StringUtils.contains(text, ".")) {
230 return StringUtils.substringBefore(text, ".") + ".";
231 }
232 else {
233 return StringUtils.trim(text);
234 }
235 }
236
237
238
239
240
241
242
243
244
245 private String buildJsonSchemaRefModel(String jsonSchemaRef, Class<?> modelClass) {
246 if (StringUtils.isNotEmpty(jsonSchemaRef)) {
247 return jsonSchemaRef;
248 }
249 if (modelClass != void.class) {
250 return project.getDependencyArtifacts().stream()
251 .filter(artifact -> StringUtils.equals("jar", artifact.getType()))
252 .map(artifact -> buildJsonSchemaRefForModel(modelClass, artifact.getFile()))
253 .filter(StringUtils::isNotEmpty)
254 .findFirst()
255 .orElse(null);
256 }
257 return null;
258 }
259
260
261
262
263
264
265
266 private String buildJsonSchemaRefForModel(Class<?> modelClass, File artifactFile) {
267 try {
268 ZipFile zipFile = new ZipFile(artifactFile);
269 String halDocsDomainPath = getHalDocsDomainPath(zipFile);
270 if (halDocsDomainPath != null && hasJsonSchemaFile(zipFile, modelClass)) {
271 return JsonSchemaBundleTracker.SCHEMA_URI_PREFIX + halDocsDomainPath
272 + "/" + modelClass.getName() + ".json";
273 }
274 return null;
275 }
276 catch (IOException ex) {
277 throw new RuntimeException("Unable to read artifact file: " + artifactFile.getAbsolutePath() + "\n" + ex.getMessage(), ex);
278 }
279 }
280
281
282
283
284
285
286
287 private String getHalDocsDomainPath(ZipFile jarFile) throws IOException {
288 ZipEntry manifestEntry = jarFile.getEntry("META-INF/MANIFEST.MF");
289 if (manifestEntry != null) {
290 try (InputStream is = jarFile.getInputStream(manifestEntry)) {
291 Manifest manifest = new Manifest(is);
292 return manifest.getMainAttributes().getValue(JsonSchemaBundleTracker.HEADER_DOMAIN_PATH);
293 }
294 }
295 return null;
296 }
297
298
299
300
301
302
303
304 private boolean hasJsonSchemaFile(ZipFile jarFile, Class<?> modelClass) throws IOException {
305 String path = JsonSchemaBundleTracker.SCHEMA_CLASSPATH_PREFIX + "/" + modelClass.getName() + ".json";
306 return jarFile.getEntry(path) != null;
307 }
308
309
310
311
312
313
314
315 private boolean hasAnnotation(JavaAnnotatedElement clazz, Class<? extends Annotation> annotationClazz) {
316 return clazz.getAnnotations().stream()
317 .filter(item -> item.getType().isA(annotationClazz.getName()))
318 .count() > 0;
319 }
320
321
322
323
324
325
326
327
328
329 @SuppressWarnings("unchecked")
330 private <T> T getStaticFieldValue(JavaClass javaClazz, JavaField javaField, ClassLoader compileClassLoader, Class<T> fieldType) {
331 try {
332 Class<?> clazz = compileClassLoader.loadClass(javaClazz.getFullyQualifiedName());
333 Field field = clazz.getField(javaField.getName());
334 return (T)field.get(fieldType);
335 }
336 catch (ClassNotFoundException | NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
337 throw new RuntimeException("Unable to get contanst value of field '" + javaClazz.getName() + "#" + javaField.getName() + ":\n" + ex.getMessage(), ex);
338 }
339 }
340
341
342
343
344
345
346
347
348
349 private <T extends Annotation> T getAnnotation(JavaClass javaClazz, JavaField javaField, ClassLoader compileClassLoader, Class<T> annotationType) {
350 try {
351 Class<?> clazz = compileClassLoader.loadClass(javaClazz.getFullyQualifiedName());
352 Field field = clazz.getField(javaField.getName());
353 return field.getAnnotation(annotationType);
354 }
355 catch (ClassNotFoundException | NoSuchFieldException | SecurityException | IllegalArgumentException ex) {
356 throw new RuntimeException("Unable to get contanst value of field '" + javaClazz.getName() + "#" + javaField.getName() + ":\n" + ex.getMessage(), ex);
357 }
358 }
359
360 @Override
361 protected String getGeneratedResourcesDirectoryPath() {
362 return generatedResourcesDirectory;
363 }
364
365 }