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}