blob: ccb13c8e748c2a4bd8e3cd61b626d45b816f21fb [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.plugins.javadoc;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Modifier;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.apache.maven.settings.Proxy;
import org.apache.maven.settings.Settings;
import org.apache.maven.shared.invoker.DefaultInvocationRequest;
import org.apache.maven.shared.invoker.DefaultInvoker;
import org.apache.maven.shared.invoker.InvocationOutputHandler;
import org.apache.maven.shared.invoker.InvocationRequest;
import org.apache.maven.shared.invoker.InvocationResult;
import org.apache.maven.shared.invoker.Invoker;
import org.apache.maven.shared.invoker.MavenInvocationException;
import org.apache.maven.shared.invoker.PrintStreamHandler;
import org.apache.maven.wagon.proxy.ProxyInfo;
import org.apache.maven.wagon.proxy.ProxyUtils;
import org.codehaus.plexus.languages.java.version.JavaVersion;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.codehaus.plexus.util.cli.CommandLineUtils;
import org.codehaus.plexus.util.cli.Commandline;
/**
* Set of utilities methods for Javadoc.
*
* @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
* @since 2.4
*/
public class JavadocUtil {
/** The default timeout used when fetching url, i.e. 2000. */
public static final int DEFAULT_TIMEOUT = 2000;
/** Error message when VM could not be started using invoker. */
protected static final String ERROR_INIT_VM =
"Error occurred during initialization of VM, try to reduce the Java heap size for the MAVEN_OPTS "
+ "environment variable using -Xms:<size> and -Xmx:<size>.";
/**
* Method that removes the invalid directories in the specified directories. <b>Note</b>: All elements in
* <code>dirs</code> could be an absolute or relative against the project's base directory <code>String</code> path.
*
* @param project the current Maven project not null
* @param dirs the collection of <code>String</code> directories path that will be validated.
* @return a List of valid <code>String</code> directories absolute paths.
*/
public static Collection<Path> pruneDirs(MavenProject project, Collection<String> dirs) {
final Path projectBasedir = project.getBasedir().toPath();
Set<Path> pruned = new LinkedHashSet<>(dirs.size());
for (String dir : dirs) {
if (dir == null) {
continue;
}
Path directory = projectBasedir.resolve(dir);
if (Files.isDirectory(directory)) {
pruned.add(directory.toAbsolutePath());
}
}
return pruned;
}
/**
* Method that removes the invalid files in the specified files. <b>Note</b>: All elements in <code>files</code>
* should be an absolute <code>String</code> path.
*
* @param files the list of <code>String</code> files paths that will be validated.
* @return a List of valid <code>File</code> objects.
*/
protected static List<String> pruneFiles(Collection<String> files) {
List<String> pruned = new ArrayList<>(files.size());
for (String f : files) {
if (!shouldPruneFile(f, pruned)) {
pruned.add(f);
}
}
return pruned;
}
/**
* Determine whether a file should be excluded from the provided list of paths, based on whether it exists and is
* already present in the list.
*
* @param f The files.
* @param pruned The list of pruned files..
* @return true if the file could be pruned false otherwise.
*/
public static boolean shouldPruneFile(String f, List<String> pruned) {
if (f != null) {
if (Files.isRegularFile(Paths.get(f)) && !pruned.contains(f)) {
return false;
}
}
return true;
}
/**
* Method that gets all the source files to be excluded from the javadoc on the given source paths.
*
* @param sourcePaths the path to the source files
* @param excludedPackages the package names to be excluded in the javadoc
* @return a List of the packages to be excluded in the generated javadoc
*/
protected static List<String> getExcludedPackages(
Collection<Path> sourcePaths, Collection<String> excludedPackages) {
List<String> excludedNames = new ArrayList<>();
for (Path sourcePath : sourcePaths) {
excludedNames.addAll(getExcludedPackages(sourcePath, excludedPackages));
}
return excludedNames;
}
/**
* Convenience method to wrap an argument value in single quotes (i.e. <code>'</code>). Intended for values which
* may contain whitespaces. <br>
* To prevent javadoc error, the line separator (i.e. <code>\n</code>) are skipped.
*
* @param value the argument value.
* @return argument with quote
*/
protected static String quotedArgument(String value) {
String arg = value;
if (arg != null && !arg.isEmpty()) {
if (arg.contains("'")) {
arg = StringUtils.replace(arg, "'", "\\'");
}
arg = "'" + arg + "'";
// To prevent javadoc error
arg = StringUtils.replace(arg, "\n", " ");
}
return arg;
}
/**
* Convenience method to format a path argument so that it is properly interpreted by the javadoc tool. Intended for
* path values which may contain whitespaces.
*
* @param value the argument value.
* @return path argument with quote
*/
protected static String quotedPathArgument(String value) {
String path = value;
if (path != null && !path.isEmpty()) {
path = path.replace('\\', '/');
if (path.contains("'")) {
StringBuilder pathBuilder = new StringBuilder();
pathBuilder.append('\'');
String[] split = path.split("'");
for (int i = 0; i < split.length; i++) {
if (i != split.length - 1) {
pathBuilder.append(split[i]).append("\\'");
} else {
pathBuilder.append(split[i]);
}
}
pathBuilder.append('\'');
path = pathBuilder.toString();
} else {
path = "'" + path + "'";
}
}
return path;
}
/**
* Convenience method that copy all <code>doc-files</code> directories from <code>javadocDir</code> to the
* <code>outputDirectory</code>.
*
* @param outputDirectory the output directory
* @param javadocDir the javadoc directory
* @param excludedocfilessubdir the excludedocfilessubdir parameter
* @throws IOException if any
* @since 2.5
*/
protected static void copyJavadocResources(File outputDirectory, File javadocDir, String excludedocfilessubdir)
throws IOException {
if (!javadocDir.isDirectory()) {
return;
}
List<String> excludes = new ArrayList<>(Arrays.asList(FileUtils.getDefaultExcludes()));
if (excludedocfilessubdir != null && !excludedocfilessubdir.isEmpty()) {
StringTokenizer st = new StringTokenizer(excludedocfilessubdir, ":");
String current;
while (st.hasMoreTokens()) {
current = st.nextToken();
excludes.add("**/" + current + "/**");
}
}
List<String> docFiles = FileUtils.getDirectoryNames(
javadocDir, "resources,**/doc-files", StringUtils.join(excludes.iterator(), ","), false, true);
for (String docFile : docFiles) {
File docFileOutput = new File(outputDirectory, docFile);
FileUtils.mkdir(docFileOutput.getAbsolutePath());
FileUtils.copyDirectoryStructure(new File(javadocDir, docFile), docFileOutput);
List<String> files = FileUtils.getFileAndDirectoryNames(
docFileOutput, StringUtils.join(excludes.iterator(), ","), null, true, true, true, true);
for (String filename : files) {
File file = new File(filename);
if (file.isDirectory()) {
FileUtils.deleteDirectory(file);
} else {
file.delete();
}
}
}
}
/**
* Method that gets the files or classes that would be included in the javadocs using the subpackages parameter.
*
* @param sourceDirectory the directory where the source files are located
* @param fileList the list of all relative files found in the sourceDirectory
* @param excludePackages package names to be excluded in the javadoc
* @return a StringBuilder that contains the appended file names of the files to be included in the javadoc
*/
protected static List<String> getIncludedFiles(
File sourceDirectory, String[] fileList, Collection<String> excludePackages) {
List<String> files = new ArrayList<>();
List<Pattern> excludePackagePatterns = new ArrayList<>(excludePackages.size());
for (String excludePackage : excludePackages) {
excludePackagePatterns.add(Pattern.compile(excludePackage
.replace('.', File.separatorChar)
.replace("\\", "\\\\")
.replace("*", ".+")
.concat("[\\\\/][^\\\\/]+\\.java")));
}
for (String file : fileList) {
boolean excluded = false;
for (Pattern excludePackagePattern : excludePackagePatterns) {
if (excludePackagePattern.matcher(file).matches()) {
excluded = true;
break;
}
}
if (!excluded) {
files.add(file.replace('\\', '/'));
}
}
return files;
}
/**
* Method that gets the complete package names (including subpackages) of the packages that were defined in the
* excludePackageNames parameter.
*
* @param sourceDirectory the directory where the source files are located
* @param excludePackagenames package names to be excluded in the javadoc
* @return a List of the packagenames to be excluded
*/
protected static Collection<String> getExcludedPackages(
final Path sourceDirectory, Collection<String> excludePackagenames) {
final String regexFileSeparator = File.separator.replace("\\", "\\\\");
final Collection<String> fileList = new ArrayList<>();
try {
Files.walkFileTree(sourceDirectory, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (file.getFileName().toString().endsWith(".java")) {
fileList.add(
sourceDirectory.relativize(file.getParent()).toString());
}
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
// noop
}
List<String> files = new ArrayList<>();
for (String excludePackagename : excludePackagenames) {
// Usage of wildcard was bad specified and bad implemented, i.e. using String.contains()
// without respecting surrounding context
// Following implementation should match requirements as defined in the examples:
// - A wildcard at the beginning should match 1 or more folders
// - Any other wildcard must match exactly one folder
Pattern p = Pattern.compile(excludePackagename
.replace(".", regexFileSeparator)
.replaceFirst("^\\*", ".+")
.replace("*", "[^" + regexFileSeparator + "]+"));
for (String aFileList : fileList) {
if (p.matcher(aFileList).matches()) {
files.add(aFileList.replace(File.separatorChar, '.'));
}
}
}
return files;
}
/**
* Convenience method that gets the files to be included in the javadoc.
*
* @param sourceDirectory the directory where the source files are located
* @param excludePackages the packages to be excluded in the javadocs
* @param sourceFileIncludes files to include.
* @param sourceFileExcludes files to exclude.
*/
protected static List<String> getFilesFromSource(
File sourceDirectory,
List<String> sourceFileIncludes,
List<String> sourceFileExcludes,
Collection<String> excludePackages) {
DirectoryScanner ds = new DirectoryScanner();
if (sourceFileIncludes == null) {
sourceFileIncludes = Collections.singletonList("**/*.java");
}
ds.setIncludes(sourceFileIncludes.toArray(new String[sourceFileIncludes.size()]));
if (sourceFileExcludes != null && sourceFileExcludes.size() > 0) {
ds.setExcludes(sourceFileExcludes.toArray(new String[sourceFileExcludes.size()]));
}
ds.setBasedir(sourceDirectory);
ds.scan();
String[] fileList = ds.getIncludedFiles();
List<String> files = new ArrayList<>();
if (fileList.length != 0) {
files.addAll(getIncludedFiles(sourceDirectory, fileList, excludePackages));
}
return files;
}
/**
* Call the Javadoc tool and parse its output to find its version, i.e.:
*
* <pre>
* javadoc.exe( or.sh ) - J - version
* </pre>
*
* @param javadocExe not null file
* @return the javadoc version as float
* @throws IOException if javadocExe is null, doesn't exist or is not a file
* @throws CommandLineException if any
* @throws IllegalArgumentException if no output was found in the command line
* @throws PatternSyntaxException if the output contains a syntax error in the regular-expression pattern.
* @see #extractJavadocVersion(String)
*/
protected static JavaVersion getJavadocVersion(File javadocExe)
throws IOException, CommandLineException, IllegalArgumentException {
if ((javadocExe == null) || (!javadocExe.exists()) || (!javadocExe.isFile())) {
throw new IOException("The javadoc executable '" + javadocExe + "' doesn't exist or is not a file. ");
}
Commandline cmd = new Commandline();
cmd.setExecutable(javadocExe.getAbsolutePath());
cmd.setWorkingDirectory(javadocExe.getParentFile());
cmd.createArg().setValue("-J-version");
CommandLineUtils.StringStreamConsumer out = new JavadocOutputStreamConsumer();
CommandLineUtils.StringStreamConsumer err = new JavadocOutputStreamConsumer();
int exitCode = CommandLineUtils.executeCommandLine(cmd, out, err);
if (exitCode != 0) {
StringBuilder msg = new StringBuilder("Exit code: " + exitCode + " - " + err.getOutput());
msg.append('\n');
msg.append("Command line was:").append(CommandLineUtils.toString(cmd.getCommandline()));
throw new CommandLineException(msg.toString());
}
if (StringUtils.isNotEmpty(err.getOutput())) {
return JavaVersion.parse(extractJavadocVersion(err.getOutput()));
} else if (StringUtils.isNotEmpty(out.getOutput())) {
return JavaVersion.parse(extractJavadocVersion(out.getOutput()));
}
throw new IllegalArgumentException("No output found from the command line 'javadoc -J-version'");
}
private static final Pattern EXTRACT_JAVADOC_VERSION_PATTERN =
Pattern.compile("(?s).*?[^a-zA-Z](([0-9]+\\.?[0-9]*)(\\.[0-9]+)?).*");
/**
* Parse the output for 'javadoc -J-version' and return the javadoc version recognized. <br>
* Here are some output for 'javadoc -J-version' depending the JDK used:
* <table><caption>Output for 'javadoc -J-version' per JDK</caption>
* <tr>
* <th>JDK</th>
* <th>Output for 'javadoc -J-version'</th>
* </tr>
* <tr>
* <td>Sun 1.4</td>
* <td>java full version "1.4.2_12-b03"</td>
* </tr>
* <tr>
* <td>Sun 1.5</td>
* <td>java full version "1.5.0_07-164"</td>
* </tr>
* <tr>
* <td>IBM 1.4</td>
* <td>javadoc full version "J2RE 1.4.2 IBM Windows 32 build cn1420-20040626"</td>
* </tr>
* <tr>
* <td>IBM 1.5 (French JVM)</td>
* <td>javadoc version complète de "J2RE 1.5.0 IBM Windows 32 build pwi32pdev-20070426a"</td>
* </tr>
* <tr>
* <td>FreeBSD 1.5</td>
* <td>java full version "diablo-1.5.0-b01"</td>
* </tr>
* <tr>
* <td>BEA jrockit 1.5</td>
* <td>java full version "1.5.0_11-b03"</td>
* </tr>
* </table>
*
* @param output for 'javadoc -J-version'
* @return the version of the javadoc for the output, only digits and dots
* @throws PatternSyntaxException if the output doesn't match with the output pattern
* {@code (?s).*?[^a-zA-Z]([0-9]+\\.?[0-9]*)(\\.([0-9]+))?.*}.
* @throws IllegalArgumentException if the output is null
*/
protected static String extractJavadocVersion(String output) throws IllegalArgumentException {
if (output == null || output.isEmpty()) {
throw new IllegalArgumentException("The output could not be null.");
}
Pattern pattern = EXTRACT_JAVADOC_VERSION_PATTERN;
Matcher matcher = pattern.matcher(output);
if (!matcher.matches()) {
throw new PatternSyntaxException(
"Unrecognized version of Javadoc: '" + output + "'",
pattern.pattern(),
pattern.toString().length() - 1);
}
return matcher.group(1);
}
private static final Pattern PARSE_JAVADOC_MEMORY_PATTERN_0 = Pattern.compile("^\\s*(\\d+)\\s*?\\s*$");
private static final Pattern PARSE_JAVADOC_MEMORY_PATTERN_1 =
Pattern.compile("^\\s*(\\d+)\\s*k(b)?\\s*$", Pattern.CASE_INSENSITIVE);
private static final Pattern PARSE_JAVADOC_MEMORY_PATTERN_2 =
Pattern.compile("^\\s*(\\d+)\\s*m(b)?\\s*$", Pattern.CASE_INSENSITIVE);
private static final Pattern PARSE_JAVADOC_MEMORY_PATTERN_3 =
Pattern.compile("^\\s*(\\d+)\\s*g(b)?\\s*$", Pattern.CASE_INSENSITIVE);
private static final Pattern PARSE_JAVADOC_MEMORY_PATTERN_4 =
Pattern.compile("^\\s*(\\d+)\\s*t(b)?\\s*$", Pattern.CASE_INSENSITIVE);
/**
* Parse a memory string which be used in the JVM arguments <code>-Xms</code> or <code>-Xmx</code>. <br>
* Here are some supported memory string depending the JDK used:
* <table><caption>Memory argument support per JDK</caption>
* <tr>
* <th>JDK</th>
* <th>Memory argument support for <code>-Xms</code> or <code>-Xmx</code></th>
* </tr>
* <tr>
* <td>SUN</td>
* <td>1024k | 128m | 1g | 1t</td>
* </tr>
* <tr>
* <td>IBM</td>
* <td>1024k | 1024b | 128m | 128mb | 1g | 1gb</td>
* </tr>
* <tr>
* <td>BEA</td>
* <td>1024k | 1024kb | 128m | 128mb | 1g | 1gb</td>
* </tr>
* </table>
*
* @param memory the memory to be parsed, not null.
* @return the memory parsed with a supported unit. If no unit specified in the <code>memory</code> parameter, the
* default unit is <code>m</code>. The units <code>g | gb</code> or <code>t | tb</code> will be converted in
* <code>m</code>.
* @throws IllegalArgumentException if the <code>memory</code> parameter is null or doesn't match any pattern.
*/
protected static String parseJavadocMemory(String memory) throws IllegalArgumentException {
if (memory == null || memory.isEmpty()) {
throw new IllegalArgumentException("The memory could not be null.");
}
Matcher m0 = PARSE_JAVADOC_MEMORY_PATTERN_0.matcher(memory);
if (m0.matches()) {
return m0.group(1) + "m";
}
Matcher m1 = PARSE_JAVADOC_MEMORY_PATTERN_1.matcher(memory);
if (m1.matches()) {
return m1.group(1) + "k";
}
Matcher m2 = PARSE_JAVADOC_MEMORY_PATTERN_2.matcher(memory);
if (m2.matches()) {
return m2.group(1) + "m";
}
Matcher m3 = PARSE_JAVADOC_MEMORY_PATTERN_3.matcher(memory);
if (m3.matches()) {
return (Integer.parseInt(m3.group(1)) * 1024) + "m";
}
Matcher m4 = PARSE_JAVADOC_MEMORY_PATTERN_4.matcher(memory);
if (m4.matches()) {
return (Integer.parseInt(m4.group(1)) * 1024 * 1024) + "m";
}
throw new IllegalArgumentException("Could convert not to a memory size: " + memory);
}
/**
* Validate if a charset is supported on this platform.
*
* @param charsetName the charsetName to be check.
* @return <code>true</code> if the given charset is supported by the JVM, <code>false</code> otherwise.
*/
protected static boolean validateEncoding(String charsetName) {
if (charsetName == null || charsetName.isEmpty()) {
return false;
}
try {
return Charset.isSupported(charsetName);
} catch (IllegalCharsetNameException e) {
return false;
}
}
/**
* Auto-detect the class names of the implementation of <code>com.sun.tools.doclets.Taglet</code> class from a given
* jar file. <br>
* <b>Note</b>: <code>JAVA_HOME/lib/tools.jar</code> is a requirement to find
* <code>com.sun.tools.doclets.Taglet</code> class.
*
* @param jarFile not null
* @return the list of <code>com.sun.tools.doclets.Taglet</code> class names from a given jarFile.
* @throws IOException if jarFile is invalid or not found, or if the <code>JAVA_HOME/lib/tools.jar</code> is not
* found.
* @throws ClassNotFoundException if any
* @throws NoClassDefFoundError if any
*/
protected static List<String> getTagletClassNames(File jarFile)
throws IOException, ClassNotFoundException, NoClassDefFoundError {
List<String> classes = getClassNamesFromJar(jarFile);
URLClassLoader cl;
// Needed to find com.sun.tools.doclets.Taglet class
File tools = new File(System.getProperty("java.home"), "../lib/tools.jar");
if (tools.exists() && tools.isFile()) {
cl = new URLClassLoader(
new URL[] {jarFile.toURI().toURL(), tools.toURI().toURL()}, null);
} else {
cl = new URLClassLoader(new URL[] {jarFile.toURI().toURL()}, ClassLoader.getSystemClassLoader());
}
List<String> tagletClasses = new ArrayList<>();
Class<?> tagletClass;
try {
tagletClass = cl.loadClass("com.sun.tools.doclets.Taglet");
} catch (ClassNotFoundException e) {
tagletClass = cl.loadClass("jdk.javadoc.doclet.Taglet");
}
for (String s : classes) {
Class<?> c = cl.loadClass(s);
if (tagletClass.isAssignableFrom(c) && !Modifier.isAbstract(c.getModifiers())) {
tagletClasses.add(c.getName());
}
}
try {
cl.close();
} catch (IOException ex) {
// no big deal
}
return tagletClasses;
}
/**
* Copy the given url to the given file.
*
* @param url not null url
* @param file not null file where the url will be created
* @throws IOException if any
* @since 2.6
*/
protected static void copyResource(URL url, File file) throws IOException {
if (file == null) {
throw new IOException("The file can't be null.");
}
if (url == null) {
throw new IOException("The url could not be null.");
}
FileUtils.copyURLToFile(url, file);
}
/**
* Invoke Maven for the given project file with a list of goals and properties, the output will be in the invokerlog
* file. <br>
* <b>Note</b>: the Maven Home should be defined in the <code>maven.home</code> Java system property or defined in
* <code>M2_HOME</code> system env variables.
*
* @param log a logger could be null.
* @param localRepositoryDir the localRepository not null.
* @param projectFile a not null project file.
* @param goals a not null goals list.
* @param properties the properties for the goals, could be null.
* @param invokerLog the log file where the invoker will be written, if null using <code>System.out</code>.
* @param globalSettingsFile reference to settings file, could be null.
* @throws MavenInvocationException if any
* @since 2.6
*/
protected static void invokeMaven(
Log log,
File localRepositoryDir,
File projectFile,
List<String> goals,
Properties properties,
File invokerLog,
File globalSettingsFile)
throws MavenInvocationException {
if (projectFile == null) {
throw new IllegalArgumentException("projectFile should be not null.");
}
if (!projectFile.isFile()) {
throw new IllegalArgumentException(projectFile.getAbsolutePath() + " is not a file.");
}
if (goals == null || goals.size() == 0) {
throw new IllegalArgumentException("goals should be not empty.");
}
if (localRepositoryDir == null || !localRepositoryDir.isDirectory()) {
throw new IllegalArgumentException(
"localRepositoryDir '" + localRepositoryDir + "' should be a directory.");
}
String mavenHome = getMavenHome(log);
if (mavenHome == null || mavenHome.isEmpty()) {
String msg = "Could NOT invoke Maven because no Maven Home is defined. You need to have set the M2_HOME "
+ "system env variable or a maven.home Java system properties.";
if (log != null) {
log.error(msg);
} else {
System.err.println(msg);
}
return;
}
Invoker invoker = new DefaultInvoker();
invoker.setMavenHome(new File(mavenHome));
invoker.setLocalRepositoryDirectory(localRepositoryDir);
InvocationRequest request = new DefaultInvocationRequest();
request.setBaseDirectory(projectFile.getParentFile());
request.setPomFile(projectFile);
request.setGlobalSettingsFile(globalSettingsFile);
request.setBatchMode(true);
if (log != null) {
request.setDebug(log.isDebugEnabled());
} else {
request.setDebug(true);
}
request.setGoals(goals);
if (properties != null) {
request.setProperties(properties);
}
File javaHome = getJavaHome(log);
if (javaHome != null) {
request.setJavaHome(javaHome);
}
if (log != null && log.isDebugEnabled()) {
log.debug("Invoking Maven for the goals: " + goals + " with "
+ (properties == null ? "no properties" : "properties=" + properties));
}
InvocationResult result = invoke(log, invoker, request, invokerLog, goals, properties, null);
if (result.getExitCode() != 0) {
String invokerLogContent = readFile(invokerLog, "UTF-8");
// see DefaultMaven
if (invokerLogContent != null
&& (!invokerLogContent.contains("Scanning for projects...")
|| invokerLogContent.contains(OutOfMemoryError.class.getName()))) {
if (log != null) {
log.error("Error occurred during initialization of VM, trying to use an empty MAVEN_OPTS...");
if (log.isDebugEnabled()) {
log.debug("Reinvoking Maven for the goals: " + goals + " with an empty MAVEN_OPTS...");
}
}
result = invoke(log, invoker, request, invokerLog, goals, properties, "");
}
}
if (result.getExitCode() != 0) {
String invokerLogContent = readFile(invokerLog, "UTF-8");
// see DefaultMaven
if (invokerLogContent != null
&& (!invokerLogContent.contains("Scanning for projects...")
|| invokerLogContent.contains(OutOfMemoryError.class.getName()))) {
throw new MavenInvocationException(ERROR_INIT_VM);
}
throw new MavenInvocationException(
"Error when invoking Maven, consult the invoker log file: " + invokerLog.getAbsolutePath());
}
}
/**
* Read the given file and return the content or null if an IOException occurs.
*
* @param javaFile not null
* @param encoding could be null
* @return the content with unified line separator of the given javaFile using the given encoding.
* @see FileUtils#fileRead(File, String)
* @since 2.6.1
*/
protected static String readFile(final File javaFile, final String encoding) {
try {
return FileUtils.fileRead(javaFile, encoding);
} catch (IOException e) {
return null;
}
}
/**
* Split the given path with colon and semi-colon, to support Solaris and Windows path. Examples:
*
* <pre>
* splitPath( "/home:/tmp" ) = ["/home", "/tmp"]
* splitPath( "/home;/tmp" ) = ["/home", "/tmp"]
* splitPath( "C:/home:C:/tmp" ) = ["C:/home", "C:/tmp"]
* splitPath( "C:/home;C:/tmp" ) = ["C:/home", "C:/tmp"]
* </pre>
*
* @param path which can contain multiple paths separated with a colon (<code>:</code>) or a semi-colon
* (<code>;</code>), platform independent. Could be null.
* @return the path splitted by colon or semi-colon or <code>null</code> if path was <code>null</code>.
* @since 2.6.1
*/
protected static String[] splitPath(final String path) {
if (path == null) {
return null;
}
List<String> subpaths = new ArrayList<>();
PathTokenizer pathTokenizer = new PathTokenizer(path);
while (pathTokenizer.hasMoreTokens()) {
subpaths.add(pathTokenizer.nextToken());
}
return subpaths.toArray(new String[subpaths.size()]);
}
/**
* Unify the given path with the current System path separator, to be platform independent. Examples:
*
* <pre>
* unifyPathSeparator( "/home:/tmp" ) = "/home:/tmp" (Solaris box)
* unifyPathSeparator( "/home:/tmp" ) = "/home;/tmp" (Windows box)
* </pre>
*
* @param path which can contain multiple paths by separating them with a colon (<code>:</code>) or a semi-colon
* (<code>;</code>), platform independent. Could be null.
* @return the same path but separated with the current System path separator or <code>null</code> if path was
* <code>null</code>.
* @since 2.6.1
* @see #splitPath(String)
* @see File#pathSeparator
*/
protected static String unifyPathSeparator(final String path) {
if (path == null) {
return null;
}
return StringUtils.join(splitPath(path), File.pathSeparator);
}
// ----------------------------------------------------------------------
// private methods
// ----------------------------------------------------------------------
/**
* @param jarFile not null
* @return all class names from the given jar file.
* @throws IOException if any or if the jarFile is null or doesn't exist.
*/
private static List<String> getClassNamesFromJar(File jarFile) throws IOException {
if (jarFile == null || !jarFile.exists() || !jarFile.isFile()) {
throw new IOException("The jar '" + jarFile + "' doesn't exist or is not a file.");
}
List<String> classes = new ArrayList<>();
Pattern pattern = Pattern.compile("(?i)^(META-INF/versions/(?<v>[0-9]+)/)?(?<n>.+)[.]class$");
try (JarInputStream jarStream = new JarInputStream(new FileInputStream(jarFile))) {
for (JarEntry jarEntry = jarStream.getNextJarEntry();
jarEntry != null;
jarEntry = jarStream.getNextJarEntry()) {
Matcher matcher = pattern.matcher(jarEntry.getName());
if (matcher.matches()) {
String version = matcher.group("v");
if ((version == null || version.isEmpty()) || JavaVersion.JAVA_VERSION.isAtLeast(version)) {
String name = matcher.group("n");
classes.add(name.replaceAll("/", "\\."));
}
}
jarStream.closeEntry();
}
}
return classes;
}
/**
* @param log could be null
* @param invoker not null
* @param request not null
* @param invokerLog not null
* @param goals not null
* @param properties could be null
* @param mavenOpts could be null
* @return the invocation result
* @throws MavenInvocationException if any
* @since 2.6
*/
private static InvocationResult invoke(
Log log,
Invoker invoker,
InvocationRequest request,
File invokerLog,
List<String> goals,
Properties properties,
String mavenOpts)
throws MavenInvocationException {
PrintStream ps;
OutputStream os = null;
if (invokerLog != null) {
if (log != null && log.isDebugEnabled()) {
log.debug("Using " + invokerLog.getAbsolutePath() + " to log the invoker");
}
try {
if (!invokerLog.exists()) {
// noinspection ResultOfMethodCallIgnored
invokerLog.getParentFile().mkdirs();
}
os = new FileOutputStream(invokerLog);
ps = new PrintStream(os, true, "UTF-8");
} catch (FileNotFoundException e) {
if (log != null && log.isErrorEnabled()) {
log.error("FileNotFoundException: " + e.getMessage() + ". Using System.out to log the invoker.");
}
ps = System.out;
} catch (UnsupportedEncodingException e) {
if (log != null && log.isErrorEnabled()) {
log.error("UnsupportedEncodingException: " + e.getMessage()
+ ". Using System.out to log the invoker.");
}
ps = System.out;
}
} else {
if (log != null && log.isDebugEnabled()) {
log.debug("Using System.out to log the invoker.");
}
ps = System.out;
}
if (mavenOpts != null) {
request.setMavenOpts(mavenOpts);
}
InvocationOutputHandler outputHandler = new PrintStreamHandler(ps, false);
request.setOutputHandler(outputHandler);
try {
outputHandler.consumeLine("Invoking Maven for the goals: " + goals + " with "
+ (properties == null ? "no properties" : "properties=" + properties));
outputHandler.consumeLine("");
outputHandler.consumeLine("M2_HOME=" + getMavenHome(log));
outputHandler.consumeLine("MAVEN_OPTS=" + getMavenOpts(log));
outputHandler.consumeLine("JAVA_HOME=" + getJavaHome(log));
outputHandler.consumeLine("JAVA_OPTS=" + getJavaOpts(log));
outputHandler.consumeLine("");
} catch (IOException ioe) {
throw new MavenInvocationException("IOException while consuming invocation output", ioe);
}
try {
return invoker.execute(request);
} finally {
IOUtil.close(os);
}
}
/**
* @param log a logger could be null
* @return the Maven home defined in the <code>maven.home</code> system property
* or null if never set.
* @since 2.6
*/
private static String getMavenHome(Log log) {
String mavenHome = System.getProperty("maven.home");
File m2Home = new File(mavenHome);
if (!m2Home.exists()) {
if (log != null && log.isErrorEnabled()) {
log.error("Cannot find Maven application directory. Either specify 'maven.home' system property.");
}
}
return mavenHome;
}
/**
* @param log a logger could be null
* @return the <code>MAVEN_OPTS</code> env variable value
* @since 2.6
*/
private static String getMavenOpts(Log log) {
return CommandLineUtils.getSystemEnvVars().getProperty("MAVEN_OPTS");
}
/**
* @param log a logger could be null
* @return the <code>JAVA_HOME</code> from System.getProperty( "java.home" ) By default,
* <code>System.getProperty( "java.home" ) = JRE_HOME</code> and <code>JRE_HOME</code> should be in the
* <code>JDK_HOME</code>
* @since 2.6
*/
private static File getJavaHome(Log log) {
File javaHome = null;
String javaHomeValue = CommandLineUtils.getSystemEnvVars().getProperty("JAVA_HOME");
// if maven.home is set, we can assume JAVA_HOME must be used for testing
if (System.getProperty("maven.home") == null || javaHomeValue == null) {
// JEP220 (Java9) restructured the JRE/JDK runtime image
if (SystemUtils.IS_OS_MAC_OSX || JavaVersion.JAVA_VERSION.isAtLeast("9")) {
javaHome = SystemUtils.getJavaHome();
} else {
javaHome = new File(SystemUtils.getJavaHome(), "..");
}
}
if (javaHome == null || !javaHome.exists()) {
javaHome = new File(javaHomeValue);
}
if (javaHome == null || !javaHome.exists()) {
if (log != null && log.isErrorEnabled()) {
log.error("Cannot find Java application directory. Either specify 'java.home' system property, or "
+ "JAVA_HOME environment variable.");
}
}
return javaHome;
}
/**
* @param log a logger could be null
* @return the <code>JAVA_OPTS</code> env variable value
* @since 2.6
*/
private static String getJavaOpts(Log log) {
return CommandLineUtils.getSystemEnvVars().getProperty("JAVA_OPTS");
}
/**
* A Path tokenizer takes a path and returns the components that make up that path. The path can use path separators
* of either ':' or ';' and file separators of either '/' or '\'.
*
* @version revision 439418 taken on 2009-09-12 from Ant Project (see
* http://svn.apache.org/repos/asf/ant/core/trunk/src/main/org/apache/tools/ant/PathTokenizer.java)
*/
private static class PathTokenizer {
/**
* A tokenizer to break the string up based on the ':' or ';' separators.
*/
private StringTokenizer tokenizer;
/**
* A String which stores any path components which have been read ahead due to DOS filesystem compensation.
*/
private String lookahead = null;
/**
* Flag to indicate whether or not we are running on a platform with a DOS style filesystem
*/
private boolean dosStyleFilesystem;
/**
* Constructs a path tokenizer for the specified path.
*
* @param path The path to tokenize. Must not be <code>null</code>.
*/
PathTokenizer(String path) {
tokenizer = new StringTokenizer(path, ":;", false);
dosStyleFilesystem = File.pathSeparatorChar == ';';
}
/**
* Tests if there are more path elements available from this tokenizer's path. If this method returns
* <code>true</code>, then a subsequent call to nextToken will successfully return a token.
*
* @return <code>true</code> if and only if there is at least one token in the string after the current
* position; <code>false</code> otherwise.
*/
public boolean hasMoreTokens() {
return lookahead != null || tokenizer.hasMoreTokens();
}
/**
* Returns the next path element from this tokenizer.
*
* @return the next path element from this tokenizer.
* @exception NoSuchElementException if there are no more elements in this tokenizer's path.
*/
public String nextToken() throws NoSuchElementException {
String token;
if (lookahead != null) {
token = lookahead;
lookahead = null;
} else {
token = tokenizer.nextToken().trim();
}
if (token.length() == 1
&& Character.isLetter(token.charAt(0))
&& dosStyleFilesystem
&& tokenizer.hasMoreTokens()) {
// we are on a dos style system so this path could be a drive
// spec. We look at the next token
String nextToken = tokenizer.nextToken().trim();
if (nextToken.startsWith("\\") || nextToken.startsWith("/")) {
// we know we are on a DOS style platform and the next path
// starts with a slash or backslash, so we know this is a
// drive spec
token += ":" + nextToken;
} else {
// store the token just read for next time
lookahead = nextToken;
}
}
return token;
}
}
/**
* Ignores line like 'Picked up JAVA_TOOL_OPTIONS: ...' as can happen on CI servers.
*
* @author Robert Scholte
* @since 3.0.1
*/
protected static class JavadocOutputStreamConsumer extends CommandLineUtils.StringStreamConsumer {
@Override
public void consumeLine(String line) {
if (!line.startsWith("Picked up ")) {
super.consumeLine(line);
}
}
}
static List<String> toList(String src) {
return toList(src, null, null);
}
static List<String> toList(String src, String elementPrefix, String elementSuffix) {
if (src == null || src.isEmpty()) {
return null;
}
List<String> result = new ArrayList<>();
StringTokenizer st = new StringTokenizer(src, "[,:;]");
StringBuilder sb = new StringBuilder(256);
while (st.hasMoreTokens()) {
sb.setLength(0);
if (elementPrefix != null && !elementPrefix.isEmpty()) {
sb.append(elementPrefix);
}
sb.append(st.nextToken());
if (elementSuffix != null && !elementSuffix.isEmpty()) {
sb.append(elementSuffix);
}
result.add(sb.toString());
}
return result;
}
static <T> List<T> toList(T[] multiple) {
return toList(null, multiple);
}
static <T> List<T> toList(T single, T[] multiple) {
if (single == null && (multiple == null || multiple.length < 1)) {
return null;
}
List<T> result = new ArrayList<>();
if (single != null) {
result.add(single);
}
if (multiple != null && multiple.length > 0) {
result.addAll(Arrays.asList(multiple));
}
return result;
}
// TODO: move to plexus-utils or use something appropriate from there
public static String toRelative(File basedir, String absolutePath) {
String relative;
absolutePath = absolutePath.replace('\\', '/');
String basedirPath = basedir.getAbsolutePath().replace('\\', '/');
if (absolutePath.startsWith(basedirPath)) {
relative = absolutePath.substring(basedirPath.length());
if (relative.startsWith("/")) {
relative = relative.substring(1);
}
if (relative.length() <= 0) {
relative = ".";
}
} else {
relative = absolutePath;
}
return relative;
}
/**
* Convenience method to determine that a collection is not empty or null.
* @param collection the collection to verify
* @return {@code true} if not {@code null} and not empty, otherwise {@code false}
*/
public static boolean isNotEmpty(final Collection<?> collection) {
return collection != null && !collection.isEmpty();
}
/**
* Convenience method to determine that a collection is empty or null.
* @param collection the collection to verify
* @return {@code true} if {@code null} or empty, otherwise {@code false}
*/
public static boolean isEmpty(final Collection<?> collection) {
return collection == null || collection.isEmpty();
}
/**
* Execute an Http request at the given URL, follows redirects, and returns the last redirect locations. For URLs
* that aren't http/https, this does nothing and simply returns the given URL unchanged.
*
* @param url URL.
* @param settings Maven settings.
* @return Last redirect location.
* @throws IOException if there was an error during the Http request.
*/
protected static URL getRedirectUrl(URL url, Settings settings) throws IOException {
String protocol = url.getProtocol();
if (!"http".equals(protocol) && !"https".equals(protocol)) {
return url;
}
try (CloseableHttpClient httpClient = createHttpClient(settings, url)) {
HttpClientContext httpContext = HttpClientContext.create();
HttpGet httpMethod = new HttpGet(url.toString());
HttpResponse response = httpClient.execute(httpMethod, httpContext);
int status = response.getStatusLine().getStatusCode();
if (status != HttpStatus.SC_OK) {
throw new FileNotFoundException(
"Unexpected HTTP status code " + status + " getting resource " + url.toExternalForm() + ".");
}
List<URI> redirects = httpContext.getRedirectLocations();
if (isEmpty(redirects)) {
return url;
} else {
URI last = redirects.get(redirects.size() - 1);
// URI must refer to directory, so prevent redirects to index.html
// see https://issues.apache.org/jira/browse/MJAVADOC-539
String truncate = "index.html";
if (last.getPath().endsWith("/" + truncate)) {
try {
String fixedPath =
last.getPath().substring(0, last.getPath().length() - truncate.length());
last = new URI(
last.getScheme(), last.getAuthority(), fixedPath, last.getQuery(), last.getFragment());
} catch (URISyntaxException ex) {
// not supposed to happen, but when it does just keep the last URI
}
}
return last.toURL();
}
}
}
/**
* Validates an <code>URL</code> to point to a valid <code>package-list</code> resource.
*
* @param url The URL to validate.
* @param settings The user settings used to configure the connection to the URL or {@code null}.
* @param validateContent <code>true</code> to validate the content of the <code>package-list</code> resource;
* <code>false</code> to only check the existence of the <code>package-list</code> resource.
* @return <code>true</code> if <code>url</code> points to a valid <code>package-list</code> resource;
* <code>false</code> else.
* @throws IOException if reading the resource fails.
* @see #createHttpClient(org.apache.maven.settings.Settings, java.net.URL)
* @since 2.8
*/
protected static boolean isValidPackageList(URL url, Settings settings, boolean validateContent)
throws IOException {
if (url == null) {
throw new IllegalArgumentException("The url is null");
}
try (BufferedReader reader = getReader(url, settings)) {
if (validateContent) {
for (String line = reader.readLine(); line != null; line = reader.readLine()) {
if (!isValidPackageName(line)) {
return false;
}
}
}
return true;
}
}
protected static boolean isValidElementList(URL url, Settings settings, boolean validateContent)
throws IOException {
if (url == null) {
throw new IllegalArgumentException("The url is null");
}
try (BufferedReader reader = getReader(url, settings)) {
if (validateContent) {
for (String line = reader.readLine(); line != null; line = reader.readLine()) {
if (line.startsWith("module:")) {
continue;
}
if (!isValidPackageName(line)) {
return false;
}
}
}
return true;
}
}
private static BufferedReader getReader(URL url, Settings settings) throws IOException {
BufferedReader reader = null;
if ("file".equals(url.getProtocol())) {
// Intentionally using the platform default encoding here since this is what Javadoc uses internally.
reader = new BufferedReader(new InputStreamReader(url.openStream()));
} else {
// http, https...
final CloseableHttpClient httpClient = createHttpClient(settings, url);
final HttpGet httpMethod = new HttpGet(url.toString());
HttpResponse response;
HttpClientContext httpContext = HttpClientContext.create();
try {
response = httpClient.execute(httpMethod, httpContext);
} catch (SocketTimeoutException e) {
// could be a sporadic failure, one more retry before we give up
response = httpClient.execute(httpMethod, httpContext);
}
int status = response.getStatusLine().getStatusCode();
if (status != HttpStatus.SC_OK) {
throw new FileNotFoundException(
"Unexpected HTTP status code " + status + " getting resource " + url.toExternalForm() + ".");
} else {
int pos = url.getPath().lastIndexOf('/');
List<URI> redirects = httpContext.getRedirectLocations();
if (pos >= 0 && isNotEmpty(redirects)) {
URI location = redirects.get(redirects.size() - 1);
String suffix = url.getPath().substring(pos);
// Redirections shall point to the same file, e.g. /package-list
if (!location.getPath().endsWith(suffix)) {
throw new FileNotFoundException(url.toExternalForm() + " redirects to "
+ location.toURL().toExternalForm() + ".");
}
}
}
// Intentionally using the platform default encoding here since this is what Javadoc uses internally.
reader = new BufferedReader(
new InputStreamReader(response.getEntity().getContent())) {
@Override
public void close() throws IOException {
super.close();
if (httpMethod != null) {
httpMethod.releaseConnection();
}
if (httpClient != null) {
httpClient.close();
}
}
};
}
return reader;
}
private static boolean isValidPackageName(String str) {
if (str == null || str.isEmpty()) {
// unnamed package is valid (even if bad practice :) )
return true;
}
int idx;
while ((idx = str.indexOf('.')) != -1) {
if (!isValidClassName(str.substring(0, idx))) {
return false;
}
str = str.substring(idx + 1);
}
return isValidClassName(str);
}
private static boolean isValidClassName(String str) {
if ((str == null || str.isEmpty()) || !Character.isJavaIdentifierStart(str.charAt(0))) {
return false;
}
for (int i = str.length() - 1; i > 0; i--) {
if (!Character.isJavaIdentifierPart(str.charAt(i))) {
return false;
}
}
return true;
}
/**
* Creates a new {@code HttpClient} instance.
*
* @param settings The settings to use for setting up the client or {@code null}.
* @param url The {@code URL} to use for setting up the client or {@code null}.
* @return A new {@code HttpClient} instance.
* @see #DEFAULT_TIMEOUT
* @since 2.8
*/
private static CloseableHttpClient createHttpClient(Settings settings, URL url) {
HttpClientBuilder builder = HttpClients.custom();
Registry<ConnectionSocketFactory> csfRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", SSLConnectionSocketFactory.getSystemSocketFactory())
.build();
builder.setConnectionManager(new PoolingHttpClientConnectionManager(csfRegistry));
builder.setDefaultRequestConfig(RequestConfig.custom()
.setSocketTimeout(DEFAULT_TIMEOUT)
.setConnectTimeout(DEFAULT_TIMEOUT)
.setCircularRedirectsAllowed(true)
.setCookieSpec(CookieSpecs.IGNORE_COOKIES)
.build());
// Some web servers don't allow the default user-agent sent by httpClient
builder.setUserAgent("Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)");
// Some server reject requests that do not have an Accept header
builder.setDefaultHeaders(Arrays.asList(new BasicHeader(HttpHeaders.ACCEPT, "*/*")));
if (settings != null && settings.getActiveProxy() != null) {
Proxy activeProxy = settings.getActiveProxy();
ProxyInfo proxyInfo = new ProxyInfo();
proxyInfo.setNonProxyHosts(activeProxy.getNonProxyHosts());
if (StringUtils.isNotEmpty(activeProxy.getHost())
&& (url == null || !ProxyUtils.validateNonProxyHosts(proxyInfo, url.getHost()))) {
HttpHost proxy = new HttpHost(activeProxy.getHost(), activeProxy.getPort());
builder.setProxy(proxy);
if (StringUtils.isNotEmpty(activeProxy.getUsername()) && activeProxy.getPassword() != null) {
Credentials credentials =
new UsernamePasswordCredentials(activeProxy.getUsername(), activeProxy.getPassword());
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY, credentials);
builder.setDefaultCredentialsProvider(credentialsProvider);
}
}
}
return builder.build();
}
static boolean equalsIgnoreCase(String value, String... strings) {
for (String s : strings) {
if (s.equalsIgnoreCase(value)) {
return true;
}
}
return false;
}
static boolean equals(String value, String... strings) {
for (String s : strings) {
if (s.equals(value)) {
return true;
}
}
return false;
}
}