001package ball.tools.javadoc;
002/*-
003 * ##########################################################################
004 * Utilities
005 * $Id: MavenTaglet.Coordinates.html 5431 2020-02-12 19:03:17Z ball $
006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/hcf-dev/blog/2019-03-30-java-interface-facades/src/main/resources/javadoc/src-html/ball/tools/javadoc/MavenTaglet.Coordinates.html $
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.InputStream;
033import java.lang.reflect.Field;
034import java.net.JarURLConnection;
035import java.net.URL;
036import java.util.Map;
037import java.util.jar.JarEntry;
038import java.util.jar.JarFile;
039import java.util.regex.Pattern;
040import java.util.stream.Stream;
041import java.util.zip.ZipEntry;
042import javax.xml.parsers.DocumentBuilderFactory;
043import javax.xml.xpath.XPath;
044import javax.xml.xpath.XPathExpression;
045import javax.xml.xpath.XPathFactory;
046import lombok.NoArgsConstructor;
047import lombok.ToString;
048import org.apache.commons.lang3.reflect.FieldUtils;
049import org.w3c.dom.Document;
050import org.w3c.dom.Element;
051import org.w3c.dom.Node;
052import org.w3c.dom.NodeList;
053
054import static javax.xml.xpath.XPathConstants.NODESET;
055import static lombok.AccessLevel.PROTECTED;
056import static org.apache.commons.lang3.StringUtils.EMPTY;
057import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
058import static org.apache.commons.lang3.StringUtils.isNotEmpty;
059
060/**
061 * Abstract base class for inline {@link Taglet}s that load
062 * {@link.uri https://maven.apache.org/index.html Maven} artifacts.
063 *
064 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
065 * @version $Revision: 5431 $
066 */
067@NoArgsConstructor(access = PROTECTED)
068public abstract class MavenTaglet extends AbstractInlineTaglet
069                                  implements SunToolsInternalToolkitTaglet {
070    private static final String PLUGIN_XML_PATH = "META-INF/maven/plugin.xml";
071    private static final String PLUGIN_MOJO_EXPRESSION_FORMAT =
072        "/plugin/mojos/mojo[implementation='%s']%s";
073    private static final XPath XPATH = XPathFactory.newInstance().newXPath();
074
075    private static final String POM_XML_NAME = "pom.xml";
076    private static final String DEPENDENCY = "dependency";
077    private static final String GROUP_ID = "groupId";
078    private static final String ARTIFACT_ID = "artifactId";
079    private static final String VERSION = "version";
080
081    protected XPathExpression compile(String format, Object... argv) {
082        XPathExpression expression = null;
083
084        try {
085            expression = XPATH.compile(String.format(format, argv));
086        } catch (Exception exception) {
087            throw new IllegalStateException(exception);
088        }
089
090        return expression;
091    }
092
093    /**
094     * Method to locate the POM from a {@link Tag}.
095     *
096     * @param   tag             The {@link Tag}.
097     *
098     * @return  The POM {@link File}.
099     *
100     * @throws  Exception       If the POM {@link File} cannot be found.
101     */
102    protected File getPomFileFor(Tag tag) throws Exception {
103        File parent = tag.position().file().getParentFile();
104        String name = defaultIfBlank(tag.text().trim(), POM_XML_NAME);
105        File file = new File(parent, name);
106
107        while (parent != null) {
108            file = new File(parent, name);
109
110            if (file.isFile()) {
111                break;
112            } else {
113                file = null;
114            }
115
116            parent = parent.getParentFile();
117        }
118
119        if (file == null || (! file.isFile())) {
120            throw new FileNotFoundException(name);
121        }
122
123        return file;
124    }
125
126    /**
127     * Inline {@link Taglet} to provide a report of fields whose values are
128     * configured by the {@link.uri https://maven.apache.org/index.html Maven}
129     * {@link.uri https://maven.apache.org/plugin-developers/index.html Plugin}
130     * {@code plugin.xml}.
131     *
132     * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
133     * @version $Revision: 5431 $
134     */
135    @ServiceProviderFor({ Taglet.class })
136    @TagletName("maven.plugin.fields")
137    @NoArgsConstructor @ToString
138    public static class PluginFields extends MavenTaglet {
139        private static final PluginFields INSTANCE = new PluginFields();
140
141        public static void register(Map<Object,Object> map) {
142            register(map, INSTANCE);
143        }
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 = getContainingClassDocFor(tag);
154            }
155
156            Class<?> type = getClassFor(doc);
157            URL url = getResourceURLOf(type);
158            Document document = null;
159
160            if (url.getProtocol().equalsIgnoreCase("file")) {
161                document =
162                    DocumentBuilderFactory.newInstance()
163                    .newDocumentBuilder()
164                    .parse(url.getPath()
165                           .replaceAll(Pattern.quote(getResourcePathOf(type)),
166                                       PLUGIN_XML_PATH));
167            } else if (url.getProtocol().equalsIgnoreCase("jar")) {
168                try (JarFile jar =
169                         ((JarURLConnection) url.openConnection()).getJarFile()) {
170                    ZipEntry entry = jar.getEntry(PLUGIN_XML_PATH);
171
172                    try (InputStream in = jar.getInputStream(entry)) {
173                        document =
174                            DocumentBuilderFactory.newInstance()
175                            .newDocumentBuilder()
176                            .parse(in);
177                    }
178                }
179            } else {
180                throw new IllegalStateException("Cannot find "
181                                                + PLUGIN_XML_PATH);
182            }
183
184            NodeList parameters =
185                (NodeList)
186                compile(PLUGIN_MOJO_EXPRESSION_FORMAT,
187                        type.getCanonicalName(),
188                        "/parameters/parameter")
189                .evaluate(document, NODESET);
190            NodeList configuration =
191                (NodeList)
192                compile(PLUGIN_MOJO_EXPRESSION_FORMAT,
193                        type.getCanonicalName(),
194                        "/configuration/*")
195                .evaluate(document, NODESET);
196
197            return div(attr("class", "summary"),
198                       h3("Maven Plugin Field Summary"),
199                       table(tag, type, configuration));
200        }
201
202        private FluentNode table(Tag tag, Class<?> type, NodeList list) {
203            return table(thead(tr(th(EMPTY), th("Field"), th("Default"))),
204                         tbody(asStream(list)
205                               .map(t -> tr(tag, type, (Element) t))));
206        }
207
208        private FluentNode tr(Tag tag, Class<?> type, Element element) {
209            FluentNode tr = fragment();
210            Field field =
211                FieldUtils.getField(type, element.getNodeName(), true);
212
213            if (field != null) {
214                tr =
215                    tr(td((! type.equals(field.getDeclaringClass()))
216                              ? type(tag, field.getDeclaringClass())
217                              : text(EMPTY)),
218                       td(declaration(tag, field)),
219                       td(code(element.getAttribute("default-value"))));
220            }
221
222            return tr;
223        }
224    }
225
226    /**
227     * {@link Taglet} to provide
228     * {@link.uri https://maven.apache.org/pom.html POM} POM coordinates as
229     * a {@code <dependency/>} element to include this documented
230     * {@link Class} or {@link Package}.
231     *
232     * <p>For example:</p>
233     *
234     * {@pom.coordinates}
235     */
236    @ServiceProviderFor({ Taglet.class })
237    @TagletName("pom.coordinates")
238    @NoArgsConstructor @ToString
239    public static class Coordinates extends MavenTaglet {
240        private static final Coordinates INSTANCE = new Coordinates();
241
242        public static void register(Map<Object,Object> map) {
243            register(map, INSTANCE);
244        }
245
246        @Override
247        public FluentNode toNode(Tag tag) throws Throwable {
248            POMProperties properties = new POMProperties();
249            Class<?> type = null;
250
251            if (tag.holder() instanceof PackageDoc) {
252                type = getClassFor((PackageDoc) tag.holder());
253            } else {
254                type = getClassFor(getContainingClassDocFor(tag));
255            }
256
257            URL url = getResourceURLOf(type);
258
259            if (url.getProtocol().equalsIgnoreCase("file")) {
260                Document document =
261                    DocumentBuilderFactory.newInstance()
262                    .newDocumentBuilder()
263                    .parse(getPomFileFor(tag));
264
265                Stream.of(GROUP_ID, ARTIFACT_ID, VERSION)
266                    .forEach(t -> properties.load(t, document, "/project/"));
267                Stream.of(VERSION)
268                    .forEach(t -> properties.load(t, document, "/project/parent/"));
269            } else if (url.getProtocol().equalsIgnoreCase("jar")) {
270                try (JarFile jar =
271                         ((JarURLConnection) url.openConnection()).getJarFile()) {
272                    JarEntry entry =
273                        jar.stream()
274                        .filter(t -> t.getName().matches("META-INF/maven/[^/]+/[^/]+/pom.properties"))
275                        .findFirst().orElse(null);
276
277                    if (entry != null) {
278                        try (InputStream in = jar.getInputStream(entry)) {
279                            properties.load(in);
280                        }
281                    }
282                }
283            }
284
285            return pre("xml",
286                       render(element(DEPENDENCY,
287                                      Stream.of(GROUP_ID, ARTIFACT_ID, VERSION)
288                                      .map(t -> element(t).content(properties.getProperty(t, "unknown")))),
289                              2));
290        }
291    }
292
293    private static class POMProperties extends PropertiesImpl {
294        private static final long serialVersionUID = -590870165719527159L;
295
296        public String load(String key, Document document, String prefix) {
297            if (document != null) {
298                computeIfAbsent(key, k -> evaluate(prefix + k, document));
299            }
300
301            return super.getProperty(key);
302        }
303
304        private String evaluate(String expression, Document document) {
305            String value = null;
306
307            try {
308                value = XPATH.evaluate(expression, document);
309            } catch (Exception exception) {
310            }
311
312            return isNotEmpty(value) ? value : null;
313        }
314    }
315}