| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.maven.tools.plugin.generator; |
| |
| import javax.swing.text.MutableAttributeSet; |
| import javax.swing.text.html.HTML; |
| import javax.swing.text.html.HTMLEditorKit; |
| import javax.swing.text.html.parser.ParserDelegator; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.StringReader; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.net.URLClassLoader; |
| import java.nio.charset.StandardCharsets; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Stack; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.apache.maven.artifact.Artifact; |
| import org.apache.maven.artifact.DependencyResolutionRequiredException; |
| import org.apache.maven.plugin.descriptor.MojoDescriptor; |
| import org.apache.maven.plugin.descriptor.PluginDescriptor; |
| import org.apache.maven.project.MavenProject; |
| import org.apache.maven.reporting.MavenReport; |
| import org.codehaus.plexus.component.repository.ComponentDependency; |
| import org.codehaus.plexus.util.StringUtils; |
| import org.codehaus.plexus.util.xml.XMLWriter; |
| import org.w3c.tidy.Tidy; |
| |
| /** |
| * Convenience methods to play with Maven plugins. |
| * |
| * @author jdcasey |
| */ |
| public final class GeneratorUtils { |
| private GeneratorUtils() { |
| // nop |
| } |
| |
| /** |
| * @param w not null writer |
| * @param pluginDescriptor not null |
| */ |
| public static void writeDependencies(XMLWriter w, PluginDescriptor pluginDescriptor) { |
| w.startElement("dependencies"); |
| |
| List<ComponentDependency> deps = pluginDescriptor.getDependencies(); |
| for (ComponentDependency dep : deps) { |
| w.startElement("dependency"); |
| |
| element(w, "groupId", dep.getGroupId()); |
| |
| element(w, "artifactId", dep.getArtifactId()); |
| |
| element(w, "type", dep.getType()); |
| |
| element(w, "version", dep.getVersion()); |
| |
| w.endElement(); |
| } |
| |
| w.endElement(); |
| } |
| |
| /** |
| * @param w not null writer |
| * @param name not null |
| * @param value could be null |
| */ |
| public static void element(XMLWriter w, String name, String value) { |
| w.startElement(name); |
| |
| if (value == null) { |
| value = ""; |
| } |
| |
| w.writeText(value); |
| |
| w.endElement(); |
| } |
| |
| /** |
| * @param artifacts not null collection of <code>Artifact</code> |
| * @return list of component dependencies, without in provided scope |
| */ |
| public static List<ComponentDependency> toComponentDependencies(Collection<Artifact> artifacts) { |
| List<ComponentDependency> componentDeps = new LinkedList<>(); |
| |
| for (Artifact artifact : artifacts) { |
| if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope())) { |
| continue; |
| } |
| |
| ComponentDependency cd = new ComponentDependency(); |
| |
| cd.setArtifactId(artifact.getArtifactId()); |
| cd.setGroupId(artifact.getGroupId()); |
| cd.setVersion(artifact.getVersion()); |
| cd.setType(artifact.getType()); |
| |
| componentDeps.add(cd); |
| } |
| |
| return componentDeps; |
| } |
| |
| /** |
| * Returns a literal replacement <code>String</code> for the specified <code>String</code>. This method |
| * produces a <code>String</code> that will work as a literal replacement <code>s</code> in the |
| * <code>appendReplacement</code> method of the {@link Matcher} class. The <code>String</code> produced will |
| * match the sequence of characters in <code>s</code> treated as a literal sequence. Slashes ('\') and dollar |
| * signs ('$') will be given no special meaning. TODO: copied from Matcher class of Java 1.5, remove once target |
| * platform can be upgraded |
| * |
| * @see <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/util/regex/Matcher.html">java.util.regex.Matcher</a> |
| * @param s The string to be literalized |
| * @return A literal string replacement |
| */ |
| private static String quoteReplacement(String s) { |
| if ((s.indexOf('\\') == -1) && (s.indexOf('$') == -1)) { |
| return s; |
| } |
| |
| StringBuilder sb = new StringBuilder(); |
| for (int i = 0; i < s.length(); i++) { |
| char c = s.charAt(i); |
| if (c == '\\') { |
| sb.append('\\'); |
| sb.append('\\'); |
| } else if (c == '$') { |
| sb.append('\\'); |
| sb.append('$'); |
| } else { |
| sb.append(c); |
| } |
| } |
| |
| return sb.toString(); |
| } |
| |
| /** |
| * Decodes javadoc inline tags into equivalent HTML tags. For instance, the inline tag "{@code <A&B>}" should be |
| * rendered as "<code><A&B></code>". |
| * |
| * @param description The javadoc description to decode, may be <code>null</code>. |
| * @return The decoded description, never <code>null</code>. |
| * @deprecated Only used for non java extractor |
| */ |
| @Deprecated |
| static String decodeJavadocTags(String description) { |
| if (description == null || description.isEmpty()) { |
| return ""; |
| } |
| |
| StringBuffer decoded = new StringBuffer(description.length() + 1024); |
| |
| Matcher matcher = Pattern.compile("\\{@(\\w+)\\s*([^\\}]*)\\}").matcher(description); |
| while (matcher.find()) { |
| String tag = matcher.group(1); |
| String text = matcher.group(2); |
| text = StringUtils.replace(text, "&", "&"); |
| text = StringUtils.replace(text, "<", "<"); |
| text = StringUtils.replace(text, ">", ">"); |
| if ("code".equals(tag)) { |
| text = "<code>" + text + "</code>"; |
| } else if ("link".equals(tag) || "linkplain".equals(tag) || "value".equals(tag)) { |
| String pattern = "(([^#\\.\\s]+\\.)*([^#\\.\\s]+))?" + "(#([^\\(\\s]*)(\\([^\\)]*\\))?\\s*(\\S.*)?)?"; |
| final int label = 7; |
| final int clazz = 3; |
| final int member = 5; |
| final int args = 6; |
| Matcher link = Pattern.compile(pattern).matcher(text); |
| if (link.matches()) { |
| text = link.group(label); |
| if (text == null || text.isEmpty()) { |
| text = link.group(clazz); |
| if (text == null || text.isEmpty()) { |
| text = ""; |
| } |
| if (StringUtils.isNotEmpty(link.group(member))) { |
| if (text != null && !text.isEmpty()) { |
| text += '.'; |
| } |
| text += link.group(member); |
| if (StringUtils.isNotEmpty(link.group(args))) { |
| text += "()"; |
| } |
| } |
| } |
| } |
| if (!"linkplain".equals(tag)) { |
| text = "<code>" + text + "</code>"; |
| } |
| } |
| matcher.appendReplacement(decoded, (text != null) ? quoteReplacement(text) : ""); |
| } |
| matcher.appendTail(decoded); |
| |
| return decoded.toString(); |
| } |
| |
| /** |
| * Fixes some javadoc comment to become a valid XHTML snippet. |
| * |
| * @param description Javadoc description with HTML tags, may be <code>null</code>. |
| * @return The description with valid XHTML tags, never <code>null</code>. |
| * @deprecated Redundant for java extractor |
| */ |
| @Deprecated |
| public static String makeHtmlValid(String description) { |
| |
| if (description == null || description.isEmpty()) { |
| return ""; |
| } |
| |
| String commentCleaned = decodeJavadocTags(description); |
| |
| // Using jTidy to clean comment |
| Tidy tidy = new Tidy(); |
| tidy.setDocType("loose"); |
| tidy.setXHTML(true); |
| tidy.setXmlOut(true); |
| tidy.setInputEncoding("UTF-8"); |
| tidy.setOutputEncoding("UTF-8"); |
| tidy.setMakeClean(true); |
| tidy.setNumEntities(true); |
| tidy.setQuoteNbsp(false); |
| tidy.setQuiet(true); |
| tidy.setShowWarnings(true); |
| |
| ByteArrayOutputStream out = new ByteArrayOutputStream(commentCleaned.length() + 256); |
| tidy.parse(new ByteArrayInputStream(commentCleaned.getBytes(StandardCharsets.UTF_8)), out); |
| commentCleaned = new String(out.toByteArray(), StandardCharsets.UTF_8); |
| |
| if (commentCleaned == null || commentCleaned.isEmpty()) { |
| return ""; |
| } |
| |
| // strip the header/body stuff |
| String ls = System.getProperty("line.separator"); |
| int startPos = commentCleaned.indexOf("<body>" + ls) + 6 + ls.length(); |
| int endPos = commentCleaned.indexOf(ls + "</body>"); |
| commentCleaned = commentCleaned.substring(startPos, endPos); |
| |
| return commentCleaned; |
| } |
| |
| /** |
| * Converts a HTML fragment as extracted from a javadoc comment to a plain text string. This method tries to retain |
| * as much of the text formatting as possible by means of the following transformations: |
| * <ul> |
| * <li>List items are converted to leading tabs (U+0009), followed by the item number/bullet, another tab and |
| * finally the item contents. Each tab denotes an increase of indentation.</li> |
| * <li>Flow breaking elements as well as literal line terminators in preformatted text are converted to a newline |
| * (U+000A) to denote a mandatory line break.</li> |
| * <li>Consecutive spaces and line terminators from character data outside of preformatted text will be normalized |
| * to a single space. The resulting space denotes a possible point for line wrapping.</li> |
| * <li>Each space in preformatted text will be converted to a non-breaking space (U+00A0).</li> |
| * </ul> |
| * |
| * @param html The HTML fragment to convert to plain text, may be <code>null</code>. |
| * @return A string with HTML tags converted into pure text, never <code>null</code>. |
| * @since 2.4.3 |
| * @deprecated Replaced by {@link HtmlToPlainTextConverter} |
| */ |
| @Deprecated |
| public static String toText(String html) { |
| if (html == null || html.isEmpty()) { |
| return ""; |
| } |
| |
| final StringBuilder sb = new StringBuilder(); |
| |
| HTMLEditorKit.Parser parser = new ParserDelegator(); |
| HTMLEditorKit.ParserCallback htmlCallback = new MojoParserCallback(sb); |
| |
| try { |
| parser.parse(new StringReader(makeHtmlValid(html)), htmlCallback, true); |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| |
| return sb.toString().replace('\"', '\''); // for CDATA |
| } |
| |
| /** |
| * ParserCallback implementation. |
| */ |
| private static class MojoParserCallback extends HTMLEditorKit.ParserCallback { |
| /** |
| * Holds the index of the current item in a numbered list. |
| */ |
| class Counter { |
| int value; |
| } |
| |
| /** |
| * A flag whether the parser is currently in the body element. |
| */ |
| private boolean body; |
| |
| /** |
| * A flag whether the parser is currently processing preformatted text, actually a counter to track nesting. |
| */ |
| private int preformatted; |
| |
| /** |
| * The current indentation depth for the output. |
| */ |
| private int depth; |
| |
| /** |
| * A stack of {@link Counter} objects corresponding to the nesting of (un-)ordered lists. A |
| * <code>null</code> element denotes an unordered list. |
| */ |
| private Stack<Counter> numbering = new Stack<>(); |
| |
| /** |
| * A flag whether an implicit line break is pending in the output buffer. This flag is used to postpone the |
| * output of implicit line breaks until we are sure that are not to be merged with other implicit line |
| * breaks. |
| */ |
| private boolean pendingNewline; |
| |
| /** |
| * A flag whether we have just parsed a simple tag. |
| */ |
| private boolean simpleTag; |
| |
| /** |
| * The current buffer. |
| */ |
| private final StringBuilder sb; |
| |
| /** |
| * @param sb not null |
| */ |
| MojoParserCallback(StringBuilder sb) { |
| this.sb = sb; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) { |
| simpleTag = true; |
| if (body && HTML.Tag.BR.equals(t)) { |
| newline(false); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) { |
| simpleTag = false; |
| if (body && (t.breaksFlow() || t.isBlock())) { |
| newline(true); |
| } |
| if (HTML.Tag.OL.equals(t)) { |
| numbering.push(new Counter()); |
| } else if (HTML.Tag.UL.equals(t)) { |
| numbering.push(null); |
| } else if (HTML.Tag.LI.equals(t)) { |
| Counter counter = numbering.peek(); |
| if (counter == null) { |
| text("-\t"); |
| } else { |
| text(++counter.value + ".\t"); |
| } |
| depth++; |
| } else if (HTML.Tag.DD.equals(t)) { |
| depth++; |
| } else if (t.isPreformatted()) { |
| preformatted++; |
| } else if (HTML.Tag.BODY.equals(t)) { |
| body = true; |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void handleEndTag(HTML.Tag t, int pos) { |
| if (HTML.Tag.OL.equals(t) || HTML.Tag.UL.equals(t)) { |
| numbering.pop(); |
| } else if (HTML.Tag.LI.equals(t) || HTML.Tag.DD.equals(t)) { |
| depth--; |
| } else if (t.isPreformatted()) { |
| preformatted--; |
| } else if (HTML.Tag.BODY.equals(t)) { |
| body = false; |
| } |
| if (body && (t.breaksFlow() || t.isBlock()) && !HTML.Tag.LI.equals(t)) { |
| if ((HTML.Tag.P.equals(t) |
| || HTML.Tag.PRE.equals(t) |
| || HTML.Tag.OL.equals(t) |
| || HTML.Tag.UL.equals(t) |
| || HTML.Tag.DL.equals(t)) |
| && numbering.isEmpty()) { |
| pendingNewline = false; |
| newline(pendingNewline); |
| } else { |
| newline(true); |
| } |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void handleText(char[] data, int pos) { |
| /* |
| * NOTE: Parsers before JRE 1.6 will parse XML-conform simple tags like <br/> as "<br>" followed by |
| * the text event ">..." so we need to watch out for the closing angle bracket. |
| */ |
| int offset = 0; |
| if (simpleTag && data[0] == '>') { |
| simpleTag = false; |
| for (++offset; offset < data.length && data[offset] <= ' '; ) { |
| offset++; |
| } |
| } |
| if (offset < data.length) { |
| String text = new String(data, offset, data.length - offset); |
| text(text); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void flush() { |
| flushPendingNewline(); |
| } |
| |
| /** |
| * Writes a line break to the plain text output. |
| * |
| * @param implicit A flag whether this is an explicit or implicit line break. Explicit line breaks are |
| * always written to the output whereas consecutive implicit line breaks are merged into a single |
| * line break. |
| */ |
| private void newline(boolean implicit) { |
| if (implicit) { |
| pendingNewline = true; |
| } else { |
| flushPendingNewline(); |
| sb.append('\n'); |
| } |
| } |
| |
| /** |
| * Flushes a pending newline (if any). |
| */ |
| private void flushPendingNewline() { |
| if (pendingNewline) { |
| pendingNewline = false; |
| if (sb.length() > 0) { |
| sb.append('\n'); |
| } |
| } |
| } |
| |
| /** |
| * Writes the specified character data to the plain text output. If the last output was a line break, the |
| * character data will automatically be prefixed with the current indent. |
| * |
| * @param data The character data, must not be <code>null</code>. |
| */ |
| private void text(String data) { |
| flushPendingNewline(); |
| if (sb.length() <= 0 || sb.charAt(sb.length() - 1) == '\n') { |
| for (int i = 0; i < depth; i++) { |
| sb.append('\t'); |
| } |
| } |
| String text; |
| if (preformatted > 0) { |
| text = data; |
| } else { |
| text = data.replace('\n', ' '); |
| } |
| sb.append(text); |
| } |
| } |
| |
| /** |
| * Find the best package name, based on the number of hits of actual Mojo classes. |
| * |
| * @param pluginDescriptor not null |
| * @return the best name of the package for the generated mojo |
| */ |
| public static String discoverPackageName(PluginDescriptor pluginDescriptor) { |
| Map<String, Integer> packageNames = new HashMap<>(); |
| |
| List<MojoDescriptor> mojoDescriptors = pluginDescriptor.getMojos(); |
| if (mojoDescriptors == null) { |
| return ""; |
| } |
| for (MojoDescriptor descriptor : mojoDescriptors) { |
| |
| String impl = descriptor.getImplementation(); |
| if (StringUtils.equals(descriptor.getGoal(), "help") && StringUtils.equals("HelpMojo", impl)) { |
| continue; |
| } |
| if (impl.lastIndexOf('.') != -1) { |
| String name = impl.substring(0, impl.lastIndexOf('.')); |
| if (packageNames.get(name) != null) { |
| int next = (packageNames.get(name)).intValue() + 1; |
| packageNames.put(name, Integer.valueOf(next)); |
| } else { |
| packageNames.put(name, Integer.valueOf(1)); |
| } |
| } else { |
| packageNames.put("", Integer.valueOf(1)); |
| } |
| } |
| |
| String packageName = ""; |
| int max = 0; |
| for (Map.Entry<String, Integer> entry : packageNames.entrySet()) { |
| int value = entry.getValue().intValue(); |
| if (value > max) { |
| max = value; |
| packageName = entry.getKey(); |
| } |
| } |
| |
| return packageName; |
| } |
| |
| /** |
| * @param impl a Mojo implementation, not null |
| * @param project a MavenProject instance, could be null |
| * @return <code>true</code> is the Mojo implementation implements <code>MavenReport</code>, |
| * <code>false</code> otherwise. |
| * @throws IllegalArgumentException if any |
| */ |
| @SuppressWarnings("unchecked") |
| public static boolean isMavenReport(String impl, MavenProject project) throws IllegalArgumentException { |
| if (impl == null) { |
| throw new IllegalArgumentException("mojo implementation should be declared"); |
| } |
| |
| ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); |
| if (project != null) { |
| List<String> classPathStrings; |
| try { |
| classPathStrings = project.getCompileClasspathElements(); |
| if (project.getExecutionProject() != null) { |
| classPathStrings.addAll(project.getExecutionProject().getCompileClasspathElements()); |
| } |
| } catch (DependencyResolutionRequiredException e) { |
| throw new IllegalArgumentException(e); |
| } |
| |
| List<URL> urls = new ArrayList<>(classPathStrings.size()); |
| for (String classPathString : classPathStrings) { |
| try { |
| urls.add(new File(classPathString).toURL()); |
| } catch (MalformedURLException e) { |
| throw new IllegalArgumentException(e); |
| } |
| } |
| |
| classLoader = new URLClassLoader(urls.toArray(new URL[urls.size()]), classLoader); |
| } |
| |
| try { |
| Class<?> clazz = Class.forName(impl, false, classLoader); |
| |
| return MavenReport.class.isAssignableFrom(clazz); |
| } catch (ClassNotFoundException e) { |
| return false; |
| } |
| } |
| } |