001package ball.tools.javadoc;
002/*-
003 * ##########################################################################
004 * Utilities
005 * $Id: MavenTaglet.java 6038 2020-05-25 06:11:37Z ball $
006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/ball-util/trunk/src/main/java/ball/tools/javadoc/MavenTaglet.java $
007 * %%
008 * Copyright (C) 2008 - 2020 Allen D. Ball
009 * %%
010 * Licensed under the Apache License, Version 2.0 (the "License");
011 * you may not use this file except in compliance with the License.
012 * You may obtain a copy of the License at
013 *
014 *      http://www.apache.org/licenses/LICENSE-2.0
015 *
016 * Unless required by applicable law or agreed to in writing, software
017 * distributed under the License is distributed on an "AS IS" BASIS,
018 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
019 * See the License for the specific language governing permissions and
020 * limitations under the License.
021 * ##########################################################################
022 */
023import ball.annotation.ServiceProviderFor;
024import ball.util.PropertiesImpl;
025import ball.xml.FluentNode;
026import com.sun.javadoc.ClassDoc;
027import com.sun.javadoc.PackageDoc;
028import com.sun.javadoc.Tag;
029import com.sun.tools.doclets.Taglet;
030import java.io.File;
031import java.io.FileNotFoundException;
032import java.io.IOException;
033import java.io.InputStream;
034import java.lang.reflect.Field;
035import java.net.JarURLConnection;
036import java.net.URL;
037import java.nio.file.Files;
038import java.nio.file.Path;
039import java.nio.file.Paths;
040import java.util.Map;
041import java.util.jar.JarEntry;
042import java.util.jar.JarFile;
043import java.util.regex.Pattern;
044import java.util.stream.Stream;
045import java.util.zip.ZipEntry;
046import javax.xml.parsers.DocumentBuilderFactory;
047import javax.xml.xpath.XPath;
048import javax.xml.xpath.XPathExpression;
049import javax.xml.xpath.XPathFactory;
050import lombok.NoArgsConstructor;
051import lombok.ToString;
052import org.apache.commons.lang3.reflect.FieldUtils;
053import org.w3c.dom.Document;
054import org.w3c.dom.Node;
055import org.w3c.dom.NodeList;
056
057import static javax.xml.xpath.XPathConstants.NODE;
058import static javax.xml.xpath.XPathConstants.NODESET;
059import static lombok.AccessLevel.PROTECTED;
060import static org.apache.commons.lang3.StringUtils.EMPTY;
061import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
062import static org.apache.commons.lang3.StringUtils.isNotEmpty;
063
064/**
065 * Abstract base class for inline {@link Taglet}s that load
066 * {@link.uri https://maven.apache.org/index.html Maven} artifacts.
067 *
068 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
069 * @version $Revision: 6038 $
070 */
071@NoArgsConstructor(access = PROTECTED)
072public abstract class MavenTaglet extends AbstractInlineTaglet
073                                  implements SunToolsInternalToolkitTaglet {
074    private static final XPath XPATH = XPathFactory.newInstance().newXPath();
075
076    private static final String POM_XML = "pom.xml";
077    private static final String DEPENDENCY = "dependency";
078    private static final String GROUP_ID = "groupId";
079    private static final String ARTIFACT_ID = "artifactId";
080    private static final String VERSION = "version";
081
082    protected XPathExpression compile(String format, Object... argv) {
083        XPathExpression expression = null;
084
085        try {
086            expression = XPATH.compile(String.format(format, argv));
087        } catch (Exception exception) {
088            throw new IllegalStateException(exception);
089        }
090
091        return expression;
092    }
093
094    /**
095     * Method to locate the POM from a {@link Tag}.
096     *
097     * @param   tag             The {@link Tag}.
098     *
099     * @return  The POM {@link File}.
100     *
101     * @throws  Exception       If the POM {@link File} cannot be found.
102     */
103    protected File getPomFileFor(Tag tag) throws Exception {
104        File parent = tag.position().file().getParentFile();
105        String name = defaultIfBlank(tag.text().trim(), POM_XML);
106        File file = new File(parent, name);
107
108        while (parent != null) {
109            file = new File(parent, name);
110
111            if (file.isFile()) {
112                break;
113            } else {
114                file = null;
115            }
116
117            parent = parent.getParentFile();
118        }
119
120        if (! (file != null && file.isFile())) {
121            throw new FileNotFoundException(name);
122        }
123
124        return file;
125    }
126
127    /**
128     * Inline {@link Taglet} to provide a report of fields whose values are
129     * configured by the {@link.uri https://maven.apache.org/index.html Maven}
130     * {@link.uri https://maven.apache.org/plugin-developers/index.html Plugin}
131     * {@code plugin.xml}.
132     */
133    @ServiceProviderFor({ Taglet.class })
134    @TagletName("maven.plugin.fields")
135    @NoArgsConstructor @ToString
136    public static class PluginFields extends MavenTaglet {
137        private static final PluginFields INSTANCE = new PluginFields();
138
139        public static void register(Map<Object,Object> map) {
140            register(map, INSTANCE);
141        }
142
143        private static final String PLUGIN_XML = "META-INF/maven/plugin.xml";
144
145        @Override
146        public FluentNode toNode(Tag tag) throws Throwable {
147            ClassDoc doc = null;
148            String[] argv = tag.text().trim().split("[\\p{Space}]+", 2);
149
150            if (isNotEmpty(argv[0])) {
151                doc = getClassDocFor(tag, argv[0]);
152            } else {
153                doc = containingClass(tag);
154            }
155
156            Class<?> type = getClassFor(doc);
157            URL url = getResourceURLOf(type);
158            Protocol protocol = Protocol.of(url);
159            Document document = null;
160
161            switch (protocol) {
162            case FILE:
163                String root =
164                    url.getPath()
165                    .replaceAll(Pattern.quote(getResourcePathOf(type)), EMPTY);
166
167                document =
168                    DocumentBuilderFactory.newInstance()
169                    .newDocumentBuilder()
170                    .parse(root + PLUGIN_XML);
171                break;
172
173            case JAR:
174                try (JarFile jar = protocol.getJarFile(url)) {
175                    ZipEntry entry = jar.getEntry(PLUGIN_XML);
176
177                    try (InputStream in = jar.getInputStream(entry)) {
178                        document =
179                            DocumentBuilderFactory.newInstance()
180                            .newDocumentBuilder()
181                            .parse(in);
182                    }
183                }
184                break;
185            }
186
187            if (document == null) {
188                throw new IllegalStateException("Cannot find " + PLUGIN_XML);
189            }
190
191            Node mojo =
192                (Node)
193                compile("/plugin/mojos/mojo[implementation='%s']",
194                        type.getCanonicalName())
195                .evaluate(document, NODE);
196
197            return div(attr("class", "summary"),
198                       h3("Maven Plugin Parameter Summary"),
199                       table(tag, type, mojo,
200                             asStream((NodeList)
201                                      compile("parameters/parameter")
202                                      .evaluate(mojo, NODESET))));
203        }
204
205        private FluentNode table(Tag tag, Class<?> type,
206                                 Node mojo, Stream<Node> parameters) {
207            return table(thead(tr(th(EMPTY), th("Field"),
208                                  th("Default"), th("Property"),
209                                  th("Required"), th("Editable"),
210                                  th("Description"))),
211                         tbody(parameters.map(t -> tr(tag, type, mojo, t))));
212        }
213
214        private FluentNode tr(Tag tag, Class<?> type,
215                              Node mojo, Node parameter) {
216            FluentNode tr = fragment();
217
218            try {
219                String name = compile("name").evaluate(parameter);
220                Field field = FieldUtils.getField(type, name, true);
221
222                if (field != null) {
223                    tr =
224                        tr(td((! type.equals(field.getDeclaringClass()))
225                                  ? type(tag, field.getDeclaringClass())
226                                  : text(EMPTY)),
227                           td(declaration(tag, field)),
228                           td(code(compile("configuration/%s/@default-value", name)
229                                   .evaluate(mojo))),
230                           td(code(compile("configuration/%s", name)
231                                   .evaluate(mojo))),
232                           td(code(compile("required").evaluate(parameter))),
233                           td(code(compile("editable").evaluate(parameter))),
234                           td(p(compile("description").evaluate(parameter))));
235                }
236            } catch (RuntimeException exception) {
237                throw exception;
238            } catch (Exception exception) {
239                throw new IllegalStateException(exception);
240            }
241
242            return tr;
243        }
244    }
245
246    /**
247     * Inline {@link Taglet} to include generated
248     * {@link.uri https://maven.apache.org/index.html Maven}
249     * {@link.uri https://maven.apache.org/plugin-developers/index.html Plugin}
250     * help documentation.
251     */
252    @ServiceProviderFor({ Taglet.class })
253    @TagletName("maven.plugin.help")
254    @NoArgsConstructor @ToString
255    public static class PluginHelp extends MavenTaglet {
256        private static final PluginHelp INSTANCE = new PluginHelp();
257
258        public static void register(Map<Object,Object> map) {
259            register(map, INSTANCE);
260        }
261
262        private static final String NAME = "plugin-help.xml";
263        private static final Pattern PATTERN =
264            Pattern.compile("META-INF/maven/(?<g>[^/]+)/(?<a>[^/]+)/"
265                            + Pattern.quote(NAME));
266
267        @Override
268        public FluentNode toNode(Tag tag) throws Throwable {
269            Class<?> type = null;
270
271            if (tag.holder() instanceof PackageDoc) {
272                type = getClassFor((PackageDoc) tag.holder());
273            } else {
274                type = getClassFor(containingClass(tag));
275            }
276
277            URL url = getResourceURLOf(type);
278            Protocol protocol = Protocol.of(url);
279            Document document = null;
280
281            switch (protocol) {
282            case FILE:
283                Path root =
284                    Paths.get(url.getPath()
285                              .replaceAll(Pattern.quote(getResourcePathOf(type)), EMPTY));
286                Path path =
287                    Files.walk(root, Integer.MAX_VALUE)
288                    .filter(Files::isRegularFile)
289                    .filter(t -> PATTERN.matcher(root.relativize(t).toString()).matches())
290                    .findFirst().orElse(null);
291
292                document =
293                    DocumentBuilderFactory.newInstance()
294                    .newDocumentBuilder()
295                    .parse(path.toFile());
296                break;
297
298            case JAR:
299                try (JarFile jar = protocol.getJarFile(url)) {
300                    JarEntry entry =
301                        jar.stream()
302                        .filter(t -> PATTERN.matcher(t.getName()).matches())
303                        .findFirst().orElse(null);
304
305                    try (InputStream in = jar.getInputStream(entry)) {
306                        document =
307                            DocumentBuilderFactory.newInstance()
308                            .newDocumentBuilder()
309                            .parse(in);
310                    }
311                }
312                break;
313            }
314
315            if (document == null) {
316                throw new IllegalStateException("Cannot find " + NAME);
317            }
318
319            return div(attr("class", "summary"),
320                       h3(compile("/plugin/name").evaluate(document)),
321                       p(compile("/plugin/description").evaluate(document)),
322                       table(tag,
323                             asStream((NodeList)
324                                      compile("/plugin/mojos/mojo")
325                                      .evaluate(document, NODESET))));
326        }
327
328        private FluentNode table(Tag tag, Stream<Node> mojos) {
329            return table(thead(tr(th("Goal"), th("Phase"), th("Description"))),
330                         tbody(mojos.map(t -> tr(tag, t))));
331        }
332
333        private FluentNode tr(Tag tag, Node mojo) {
334            FluentNode tr = fragment();
335
336            try {
337                tr =
338                    tr(td(a(tag,
339                            compile("implementation").evaluate(mojo),
340                            code(compile("goal").evaluate(mojo)))),
341                       td(code(compile("phase").evaluate(mojo))),
342                       td(p(code(compile("description").evaluate(mojo)))));
343            } catch (RuntimeException exception) {
344                throw exception;
345            } catch (Exception exception) {
346                throw new IllegalStateException(exception);
347            }
348
349            return tr;
350        }
351    }
352
353    /**
354     * {@link Taglet} to provide
355     * {@link.uri https://maven.apache.org/pom.html POM} POM coordinates as
356     * a {@code <dependency/>} element to include this documented
357     * {@link Class} or {@link Package}.
358     *
359     * <p>For example:</p>
360     *
361     * {@pom.coordinates}
362     */
363    @ServiceProviderFor({ Taglet.class })
364    @TagletName("pom.coordinates")
365    @NoArgsConstructor @ToString
366    public static class Coordinates extends MavenTaglet {
367        private static final Coordinates INSTANCE = new Coordinates();
368
369        public static void register(Map<Object,Object> map) {
370            register(map, INSTANCE);
371        }
372
373        private static final Pattern PATTERN =
374            Pattern.compile("META-INF/maven/(?<g>[^/]+)/(?<a>[^/]+)/pom[.]properties");
375
376        @Override
377        public FluentNode toNode(Tag tag) throws Throwable {
378            POMProperties properties = new POMProperties();
379            Class<?> type = null;
380
381            if (tag.holder() instanceof PackageDoc) {
382                type = getClassFor((PackageDoc) tag.holder());
383            } else {
384                type = getClassFor(containingClass(tag));
385            }
386
387            URL url = getResourceURLOf(type);
388            Protocol protocol = Protocol.of(url);
389
390            switch (protocol) {
391            case FILE:
392                Document document =
393                    DocumentBuilderFactory.newInstance()
394                    .newDocumentBuilder()
395                    .parse(getPomFileFor(tag));
396
397                Stream.of(GROUP_ID, ARTIFACT_ID, VERSION)
398                    .forEach(t -> properties.load(t, document, "/project/"));
399                Stream.of(VERSION)
400                    .forEach(t -> properties.load(t, document, "/project/parent/"));
401                break;
402
403            case JAR:
404                try (JarFile jar = protocol.getJarFile(url)) {
405                    JarEntry entry =
406                        jar.stream()
407                        .filter(t -> PATTERN.matcher(t.getName()).matches())
408                        .findFirst().orElse(null);
409
410                    if (entry != null) {
411                        try (InputStream in = jar.getInputStream(entry)) {
412                            properties.load(in);
413                        }
414                    }
415                }
416                break;
417            }
418
419            return pre("xml",
420                       render(element(DEPENDENCY,
421                                      Stream.of(GROUP_ID, ARTIFACT_ID, VERSION)
422                                      .map(t -> element(t).content(properties.getProperty(t, "unknown")))),
423                              2));
424        }
425    }
426
427    private static enum Protocol {
428        FILE, JAR;
429
430        public static Protocol of(URL url) {
431            return valueOf(url.getProtocol().toUpperCase());
432        }
433
434        public JarFile getJarFile(URL url) throws IOException {
435            JarFile jar = null;
436
437            switch (this) {
438            case JAR:
439                jar = ((JarURLConnection) url.openConnection()).getJarFile();
440                break;
441
442            default:
443                throw new IllegalStateException();
444                /* break; */
445            }
446
447            return jar;
448        }
449    }
450
451    private static class POMProperties extends PropertiesImpl {
452        private static final long serialVersionUID = 1354153174028868013L;
453
454        public String load(String key, Document document, String prefix) {
455            if (document != null) {
456                computeIfAbsent(key, k -> evaluate(prefix + k, document));
457            }
458
459            return super.getProperty(key);
460        }
461
462        private String evaluate(String expression, Document document) {
463            String value = null;
464
465            try {
466                value = XPATH.evaluate(expression, document);
467            } catch (Exception exception) {
468            }
469
470            return isNotEmpty(value) ? value : null;
471        }
472    }
473}