/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch 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.elasticsearch.bootstrap;
import org.elasticsearch.Version;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.PathUtils;
import org.elasticsearch.test.ESTestCase;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class JarHellTests extends ESTestCase {
URL makeJar(Path dir, String name, Manifest manifest, String... files) throws IOException {
Path jarpath = dir.resolve(name);
ZipOutputStream out;
if (manifest == null) {
out = new JarOutputStream(Files.newOutputStream(jarpath, StandardOpenOption.CREATE));
} else {
out = new JarOutputStream(Files.newOutputStream(jarpath, StandardOpenOption.CREATE), manifest);
}
for (String file : files) {
out.putNextEntry(new ZipEntry(file));
}
out.close();
return jarpath.toUri().toURL();
}
URL makeFile(Path dir, String name) throws IOException {
Path filepath = dir.resolve(name);
Files.newOutputStream(filepath, StandardOpenOption.CREATE).close();
return dir.toUri().toURL();
}
public void testDifferentJars() throws Exception {
Path dir = createTempDir();
Set<URL> jars = asSet(makeJar(dir, "foo.jar", null, "DuplicateClass.class"),
makeJar(dir, "bar.jar", null, "DuplicateClass.class"));
try {
JarHell.checkJarHell(jars);
fail("did not get expected exception");
} catch (IllegalStateException e) {
assertTrue(e.getMessage().contains("jar hell!"));
assertTrue(e.getMessage().contains("DuplicateClass"));
assertTrue(e.getMessage().contains("foo.jar"));
assertTrue(e.getMessage().contains("bar.jar"));
}
}
public void testDirsOnClasspath() throws Exception {
Path dir1 = createTempDir();
Path dir2 = createTempDir();
Set<URL> dirs = asSet(makeFile(dir1, "DuplicateClass.class"),
makeFile(dir2, "DuplicateClass.class"));
try {
JarHell.checkJarHell(dirs);
fail("did not get expected exception");
} catch (IllegalStateException e) {
assertTrue(e.getMessage().contains("jar hell!"));
assertTrue(e.getMessage().contains("DuplicateClass"));
assertTrue(e.getMessage().contains(dir1.toString()));
assertTrue(e.getMessage().contains(dir2.toString()));
}
}
public void testDirAndJar() throws Exception {
Path dir1 = createTempDir();
Path dir2 = createTempDir();
Set<URL> dirs = asSet(makeJar(dir1, "foo.jar", null, "DuplicateClass.class"),
makeFile(dir2, "DuplicateClass.class"));
try {
JarHell.checkJarHell(dirs);
fail("did not get expected exception");
} catch (IllegalStateException e) {
assertTrue(e.getMessage().contains("jar hell!"));
assertTrue(e.getMessage().contains("DuplicateClass"));
assertTrue(e.getMessage().contains("foo.jar"));
assertTrue(e.getMessage().contains(dir2.toString()));
}
}
public void testWithinSingleJar() throws Exception {
// the java api for zip file does not allow creating duplicate entries (good!) so
// this bogus jar had to be with https://github.com/jasontedor/duplicate-classes
Set<URL> jars = Collections.singleton(JarHellTests.class.getResource("duplicate-classes.jar"));
try {
JarHell.checkJarHell(jars);
fail("did not get expected exception");
} catch (IllegalStateException e) {
assertTrue(e.getMessage().contains("jar hell!"));
assertTrue(e.getMessage().contains("DuplicateClass"));
assertTrue(e.getMessage().contains("duplicate-classes.jar"));
assertTrue(e.getMessage().contains("exists multiple times in jar"));
}
}
public void testXmlBeansLeniency() throws Exception {
Set<URL> jars = Collections.singleton(JarHellTests.class.getResource("duplicate-xmlbeans-classes.jar"));
JarHell.checkJarHell(jars);
}
public void testRequiredJDKVersionTooOld() throws Exception {
Path dir = createTempDir();
List<Integer> current = JavaVersion.current().getVersion();
List<Integer> target = new ArrayList<>(current.size());
for (int i = 0; i < current.size(); i++) {
target.add(current.get(i) + 1);
}
JavaVersion targetVersion = JavaVersion.parse(Strings.collectionToDelimitedString(target, "."));
Manifest manifest = new Manifest();
Attributes attributes = manifest.getMainAttributes();
attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0.0");
attributes.put(new Attributes.Name("X-Compile-Target-JDK"), targetVersion.toString());
Set<URL> jars = Collections.singleton(makeJar(dir, "foo.jar", manifest, "Foo.class"));
try {
JarHell.checkJarHell(jars);
fail("did not get expected exception");
} catch (IllegalStateException e) {
assertTrue(e.getMessage().contains("requires Java " + targetVersion.toString()));
assertTrue(e.getMessage().contains("your system: " + JavaVersion.current().toString()));
}
}
public void testBadJDKVersionInJar() throws Exception {
Path dir = createTempDir();
Manifest manifest = new Manifest();
Attributes attributes = manifest.getMainAttributes();
attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0.0");
attributes.put(new Attributes.Name("X-Compile-Target-JDK"), "bogus");
Set<URL> jars = Collections.singleton(makeJar(dir, "foo.jar", manifest, "Foo.class"));
try {
JarHell.checkJarHell(jars);
fail("did not get expected exception");
} catch (IllegalStateException e) {
assertTrue(e.getMessage().equals("version string must be a sequence of nonnegative decimal integers separated by \".\"'s and may have leading zeros but was bogus"));
}
}
public void testRequiredJDKVersionIsOK() throws Exception {
Path dir = createTempDir();
Manifest manifest = new Manifest();
Attributes attributes = manifest.getMainAttributes();
attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0.0");
attributes.put(new Attributes.Name("X-Compile-Target-JDK"), "1.7");
Set<URL> jars = Collections.singleton(makeJar(dir, "foo.jar", manifest, "Foo.class"));
JarHell.checkJarHell(jars);
}
/** make sure if a plugin is compiled against the same ES version, it works */
public void testGoodESVersionInJar() throws Exception {
Path dir = createTempDir();
Manifest manifest = new Manifest();
Attributes attributes = manifest.getMainAttributes();
attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0.0");
attributes.put(new Attributes.Name("X-Compile-Elasticsearch-Version"), Version.CURRENT.toString());
Set<URL> jars = Collections.singleton(makeJar(dir, "foo.jar", manifest, "Foo.class"));
JarHell.checkJarHell(jars);
}
/** make sure if a plugin is compiled against a different ES version, it fails */
public void testBadESVersionInJar() throws Exception {
Path dir = createTempDir();
Manifest manifest = new Manifest();
Attributes attributes = manifest.getMainAttributes();
attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0.0");
attributes.put(new Attributes.Name("X-Compile-Elasticsearch-Version"), "1.0-bogus");
Set<URL> jars = Collections.singleton(makeJar(dir, "foo.jar", manifest, "Foo.class"));
try {
JarHell.checkJarHell(jars);
fail("did not get expected exception");
} catch (IllegalStateException e) {
assertTrue(e.getMessage().contains("requires Elasticsearch 1.0-bogus"));
}
}
public void testValidVersions() {
String[] versions = new String[]{"1.7", "1.7.0", "0.1.7", "1.7.0.80"};
for (String version : versions) {
try {
JarHell.checkVersionFormat(version);
} catch (IllegalStateException e) {
fail(version + " should be accepted as a valid version format");
}
}
}
public void testInvalidVersions() {
String[] versions = new String[]{"", "1.7.0_80", "1.7."};
for (String version : versions) {
try {
JarHell.checkVersionFormat(version);
fail("\"" + version + "\"" + " should be rejected as an invalid version format");
} catch (IllegalStateException e) {
}
}
}
// classpath testing is system specific, so we just write separate tests for *nix and windows cases
/**
* Parse a simple classpath with two elements on unix
*/
public void testParseClassPathUnix() throws Exception {
assumeTrue("test is designed for unix-like systems only", ":".equals(System.getProperty("path.separator")));
assumeTrue("test is designed for unix-like systems only", "/".equals(System.getProperty("file.separator")));
Path element1 = createTempDir();
Path element2 = createTempDir();
Set<URL> expected = asSet(element1.toUri().toURL(), element2.toUri().toURL());
assertEquals(expected, JarHell.parseClassPath(element1.toString() + ":" + element2.toString()));
}
/**
* Make sure an old unix classpath with an empty element (implicitly CWD: i'm looking at you 1.x ES scripts) fails
*/
public void testEmptyClassPathUnix() throws Exception {
assumeTrue("test is designed for unix-like systems only", ":".equals(System.getProperty("path.separator")));
assumeTrue("test is designed for unix-like systems only", "/".equals(System.getProperty("file.separator")));
try {
JarHell.parseClassPath(":/element1:/element2");
fail("should have hit exception");
} catch (IllegalStateException expected) {
assertTrue(expected.getMessage().contains("should not contain empty elements"));
}
}
/**
* Parse a simple classpath with two elements on windows
*/
public void testParseClassPathWindows() throws Exception {
assumeTrue("test is designed for windows-like systems only", ";".equals(System.getProperty("path.separator")));
assumeTrue("test is designed for windows-like systems only", "\\".equals(System.getProperty("file.separator")));
Path element1 = createTempDir();
Path element2 = createTempDir();
Set<URL> expected = asSet(element1.toUri().toURL(), element2.toUri().toURL());
assertEquals(expected, JarHell.parseClassPath(element1.toString() + ";" + element2.toString()));
}
/**
* Make sure an old windows classpath with an empty element (implicitly CWD: i'm looking at you 1.x ES scripts) fails
*/
public void testEmptyClassPathWindows() throws Exception {
assumeTrue("test is designed for windows-like systems only", ";".equals(System.getProperty("path.separator")));
assumeTrue("test is designed for windows-like systems only", "\\".equals(System.getProperty("file.separator")));
try {
JarHell.parseClassPath(";c:\\element1;c:\\element2");
fail("should have hit exception");
} catch (IllegalStateException expected) {
assertTrue(expected.getMessage().contains("should not contain empty elements"));
}
}
/**
* Make sure a "bogus" windows classpath element is accepted, java's classpath parsing accepts it,
* therefore eclipse OSGI code does it :)
*/
public void testCrazyEclipseClassPathWindows() throws Exception {
assumeTrue("test is designed for windows-like systems only", ";".equals(System.getProperty("path.separator")));
assumeTrue("test is designed for windows-like systems only", "\\".equals(System.getProperty("file.separator")));
Set<URL> expected = asSet(
PathUtils.get("c:\\element1").toUri().toURL(),
PathUtils.get("c:\\element2").toUri().toURL(),
PathUtils.get("c:\\element3").toUri().toURL(),
PathUtils.get("c:\\element 4").toUri().toURL()
);
Set<URL> actual = JarHell.parseClassPath("c:\\element1;c:\\element2;/c:/element3;/c:/element 4");
assertEquals(expected, actual);
}
}