blob: c9779870288d1b701bf6f86b6981af15d098d48b [file] [log] [blame]
/*
* 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.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
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.plugin.descriptor.MojoDescriptor;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.project.MavenProject;
import org.apache.maven.tools.plugin.util.PluginUtils;
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>&lt;A&amp;B&gt;</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, "&", "&amp;");
text = StringUtils.replace(text, "<", "&lt;");
text = StringUtils.replace(text, ">", "&gt;");
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
* @deprecated Use {@link PluginUtils#isMavenReport(String, MavenProject)} instead.
*/
@Deprecated
public static boolean isMavenReport(String impl, MavenProject project) throws IllegalArgumentException {
return PluginUtils.isMavenReport(impl, project);
}
}