diff --git a/resource-providers/README.md b/resource-providers/README.md index 4c1882edf..8eb8fd346 100644 --- a/resource-providers/README.md +++ b/resource-providers/README.md @@ -10,10 +10,14 @@ to populate the `service.name` resource attribute based on the runtime configura of an app server. This is useful when a user has not yet specified the `service.name` resource attribute manually. -It is capable of detecting common scenarios among the following popular application servers: +This `ResourceProvider` supports `.ear` and `.war` archives as well as exploded directory +versions of each. For `.war` files, it attempts to parse the `` element +from `WEB-INF/web.xml`. For `.ear` files the `` element of `META-INF/application.xml`. -* tbd (will be filled in as implementations are added) +It is capable of detecting common scenarios among the popular application servers listed below: +* GlassFish +* _remaining are tbd_ ## Component owners diff --git a/resource-providers/src/main/java/io/opentelemetry/resourceproviders/AppServer.java b/resource-providers/src/main/java/io/opentelemetry/resourceproviders/AppServer.java index 671bc57ee..679dd5c96 100644 --- a/resource-providers/src/main/java/io/opentelemetry/resourceproviders/AppServer.java +++ b/resource-providers/src/main/java/io/opentelemetry/resourceproviders/AppServer.java @@ -14,12 +14,14 @@ interface AppServer { /** Path to directory to be scanned for deployments. */ + @Nullable Path getDeploymentDir() throws Exception; /** * Returns a single class that, when present, determines that the given application server is * active/running. */ + @Nullable Class getServerClass(); /** diff --git a/resource-providers/src/main/java/io/opentelemetry/resourceproviders/AppServerServiceNameDetector.java b/resource-providers/src/main/java/io/opentelemetry/resourceproviders/AppServerServiceNameDetector.java new file mode 100644 index 000000000..94aafc7ba --- /dev/null +++ b/resource-providers/src/main/java/io/opentelemetry/resourceproviders/AppServerServiceNameDetector.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.resourceproviders; + +import static java.util.logging.Level.FINE; + +import com.google.errorprone.annotations.MustBeClosed; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; +import java.util.logging.Logger; +import java.util.stream.Stream; +import javax.annotation.Nullable; + +final class AppServerServiceNameDetector implements ServiceNameDetector { + + private static final Logger logger = + Logger.getLogger(AppServerServiceNameDetector.class.getName()); + + private final AppServer appServer; + private final ParseBuddy parseBuddy; + private final DirectoryTool dirTool; + + AppServerServiceNameDetector(AppServer appServer) { + this(appServer, new ParseBuddy(appServer), new DirectoryTool()); + } + + // Exists for testing + AppServerServiceNameDetector(AppServer appServer, ParseBuddy parseBuddy, DirectoryTool dirTool) { + this.appServer = appServer; + this.parseBuddy = parseBuddy; + this.dirTool = dirTool; + } + + @Override + @Nullable + public String detect() throws Exception { + if (appServer.getServerClass() == null) { + return null; + } + + Path deploymentDir = appServer.getDeploymentDir(); + if (deploymentDir == null) { + return null; + } + + if (!dirTool.isDirectory(deploymentDir)) { + logger.log(FINE, "Deployment dir '{0}' doesn't exist.", deploymentDir); + return null; + } + + logger.log(FINE, "Looking for deployments in '{0}'.", deploymentDir); + try (Stream stream = dirTool.list(deploymentDir)) { + return stream.map(this::detectName).filter(Objects::nonNull).findFirst().orElse(null); + } + } + + @Nullable + private String detectName(Path path) { + if (!appServer.isValidAppName(path)) { + logger.log(FINE, "Skipping '{0}'.", path); + return null; + } + + logger.log(FINE, "Attempting service name detection in '{0}'.", path); + String name = path.getFileName().toString(); + if (dirTool.isDirectory(path)) { + return parseBuddy.handleExplodedApp(path); + } + if (name.endsWith(".war")) { + return parseBuddy.handlePackagedWar(path); + } + if (appServer.supportsEar() && name.endsWith(".ear")) { + return parseBuddy.handlePackagedEar(path); + } + + return null; + } + + // Exists for testing + static class DirectoryTool { + boolean isDirectory(Path path) { + return Files.isDirectory(path); + } + + @MustBeClosed + Stream list(Path path) throws IOException { + return Files.list(path); + } + } +} diff --git a/resource-providers/src/main/java/io/opentelemetry/resourceproviders/CommonAppServersServiceNameDetector.java b/resource-providers/src/main/java/io/opentelemetry/resourceproviders/CommonAppServersServiceNameDetector.java index e6fceaf34..9006199f4 100644 --- a/resource-providers/src/main/java/io/opentelemetry/resourceproviders/CommonAppServersServiceNameDetector.java +++ b/resource-providers/src/main/java/io/opentelemetry/resourceproviders/CommonAppServersServiceNameDetector.java @@ -5,8 +5,10 @@ package io.opentelemetry.resourceproviders; +import java.net.URL; import java.util.Collections; import java.util.List; +import javax.annotation.Nullable; /** * This class is just a factory that provides a ServiceNameDetector that knows how to find and parse @@ -21,7 +23,30 @@ static ServiceNameDetector create() { private CommonAppServersServiceNameDetector() {} private static List detectors() { - // TBD: This will contain common app server detector implementations - return Collections.emptyList(); + ResourceLocator locator = new ResourceLocatorImpl(); + // Additional implementations will be added to this list. + return Collections.singletonList(detectorFor(new GlassfishAppServer(locator))); + } + + private static AppServerServiceNameDetector detectorFor(AppServer appServer) { + return new AppServerServiceNameDetector(appServer); + } + + private static class ResourceLocatorImpl implements ResourceLocator { + + @Override + @Nullable + public Class findClass(String className) { + try { + return Class.forName(className, false, ClassLoader.getSystemClassLoader()); + } catch (ClassNotFoundException | LinkageError exception) { + return null; + } + } + + @Override + public URL getClassLocation(Class clazz) { + return clazz.getProtectionDomain().getCodeSource().getLocation(); + } } } diff --git a/resource-providers/src/main/java/io/opentelemetry/resourceproviders/GlassfishAppServer.java b/resource-providers/src/main/java/io/opentelemetry/resourceproviders/GlassfishAppServer.java new file mode 100644 index 000000000..1387cea12 --- /dev/null +++ b/resource-providers/src/main/java/io/opentelemetry/resourceproviders/GlassfishAppServer.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.resourceproviders; + +import java.nio.file.Path; +import java.nio.file.Paths; +import javax.annotation.Nullable; + +class GlassfishAppServer implements AppServer { + + private static final String SERVICE_CLASS_NAME = "com.sun.enterprise.glassfish.bootstrap.ASMain"; + private final ResourceLocator locator; + + GlassfishAppServer(ResourceLocator locator) { + this.locator = locator; + } + + @Nullable + @Override + public Path getDeploymentDir() { + String instanceRoot = System.getProperty("com.sun.aas.instanceRoot"); + if (instanceRoot == null) { + return null; + } + + // besides autodeploy directory it is possible to deploy applications through admin console and + // asadmin script, to detect those we would need to parse config/domain.xml + return Paths.get(instanceRoot, "autodeploy"); + } + + @Override + @Nullable + public Class getServerClass() { + return locator.findClass(SERVICE_CLASS_NAME); + } +} diff --git a/resource-providers/src/main/java/io/opentelemetry/resourceproviders/ParseBuddy.java b/resource-providers/src/main/java/io/opentelemetry/resourceproviders/ParseBuddy.java new file mode 100644 index 000000000..8b809f139 --- /dev/null +++ b/resource-providers/src/main/java/io/opentelemetry/resourceproviders/ParseBuddy.java @@ -0,0 +1,211 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.resourceproviders; + +import static java.util.logging.Level.FINE; +import static java.util.logging.Level.WARNING; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import javax.annotation.Nullable; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** Helper class for parsing webserver xml files from various locations. */ +class ParseBuddy { + + private static final Logger logger = Logger.getLogger(ParseBuddy.class.getName()); + + private final AppServer appServer; + private final Filesystem filesystem; + + ParseBuddy(AppServer appServer) { + this(appServer, new Filesystem()); + } + + // Exists for testing + ParseBuddy(AppServer appServer, Filesystem filesystem) { + this.appServer = appServer; + this.filesystem = filesystem; + } + + @Nullable + String handleExplodedApp(Path path) { + String warResult = handleExplodedWar(path); + if (warResult != null) { + return warResult; + } + if (appServer.supportsEar()) { + return handleExplodedEar(path); + } + return null; + } + + @Nullable + String handlePackagedWar(Path path) { + return handlePackaged(path, "WEB-INF/web.xml", newWebXmlHandler()); + } + + @Nullable + String handlePackagedEar(Path path) { + return handlePackaged(path, "META-INF/application.xml", newAppXmlHandler()); + } + + @Nullable + private String handlePackaged(Path path, String descriptorPath, DescriptorHandler handler) { + try (ZipFile zip = filesystem.openZipFile(path)) { + ZipEntry zipEntry = zip.getEntry(descriptorPath); + if (zipEntry != null) { + return handle(() -> zip.getInputStream(zipEntry), path, handler); + } + } catch (IOException exception) { + if (logger.isLoggable(WARNING)) { + logger.log( + WARNING, "Failed to read '" + descriptorPath + "' from zip '" + path + "'.", exception); + } + } + + return null; + } + + @Nullable + String handleExplodedWar(Path path) { + return handleExploded(path, path.resolve("WEB-INF/web.xml"), newWebXmlHandler()); + } + + @Nullable + String handleExplodedEar(Path path) { + return handleExploded(path, path.resolve("META-INF/application.xml"), newAppXmlHandler()); + } + + @Nullable + private String handleExploded(Path path, Path descriptor, DescriptorHandler handler) { + if (filesystem.isRegularFile(descriptor)) { + return handle(() -> filesystem.newInputStream(descriptor), path, handler); + } + + return null; + } + + @Nullable + private String handle(InputStreamSupplier supplier, Path path, DescriptorHandler handler) { + try { + try (InputStream inputStream = supplier.supply()) { + String candidate = parseDescriptor(inputStream, handler); + if (appServer.isValidResult(path, candidate)) { + return candidate; + } + } + } catch (Exception exception) { + logger.log(WARNING, "Failed to parse descriptor", exception); + } + + return null; + } + + @Nullable + private static String parseDescriptor(InputStream inputStream, DescriptorHandler handler) + throws ParserConfigurationException, SAXException, IOException { + if (SaxParserFactoryHolder.saxParserFactory == null) { + return null; + } + SAXParser saxParser = SaxParserFactoryHolder.saxParserFactory.newSAXParser(); + saxParser.parse(inputStream, handler); + return handler.displayName; + } + + private interface InputStreamSupplier { + InputStream supply() throws IOException; + } + + private static DescriptorHandler newWebXmlHandler() { + return new DescriptorHandler("web-app"); + } + + private static DescriptorHandler newAppXmlHandler() { + return new DescriptorHandler("application"); + } + + private static final class DescriptorHandler extends DefaultHandler { + private final String rootElementName; + private final Deque currentElement = new ArrayDeque<>(); + private boolean setDisplayName; + @Nullable private String displayName; + + DescriptorHandler(String rootElementName) { + this.rootElementName = rootElementName; + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) { + if (displayName == null + && rootElementName.equals(currentElement.peek()) + && "display-name".equals(qName)) { + String lang = attributes.getValue("xml:lang"); + if (lang == null || "".equals(lang)) { + lang = "en"; // en is the default language + } + if ("en".equals(lang)) { + setDisplayName = true; + } + } + currentElement.push(qName); + } + + @Override + public void endElement(String uri, String localName, String qName) { + currentElement.pop(); + setDisplayName = false; + } + + @Override + public void characters(char[] ch, int start, int length) { + if (setDisplayName) { + displayName = new String(ch, start, length); + } + } + } + + private static class SaxParserFactoryHolder { + @Nullable private static final SAXParserFactory saxParserFactory = getSaxParserFactory(); + + @Nullable + private static SAXParserFactory getSaxParserFactory() { + try { + return SAXParserFactory.newInstance(); + } catch (Throwable throwable) { + logger.log(FINE, "XML parser not available.", throwable); + } + return null; + } + } + + // Exists for testing + static class Filesystem { + boolean isRegularFile(Path path) { + return Files.isRegularFile(path); + } + + InputStream newInputStream(Path path) throws IOException { + return Files.newInputStream(path); + } + + ZipFile openZipFile(Path path) throws IOException { + return new ZipFile(path.toFile()); + } + } +} diff --git a/resource-providers/src/main/java/io/opentelemetry/resourceproviders/ResourceLocator.java b/resource-providers/src/main/java/io/opentelemetry/resourceproviders/ResourceLocator.java new file mode 100644 index 000000000..3fdaa79a3 --- /dev/null +++ b/resource-providers/src/main/java/io/opentelemetry/resourceproviders/ResourceLocator.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.resourceproviders; + +import java.net.URL; +import javax.annotation.Nullable; + +interface ResourceLocator { + + @Nullable + Class findClass(String className); + + URL getClassLocation(Class clazz); +} diff --git a/resource-providers/src/test/java/io/opentelemetry/resourceproviders/AppServerServiceNameDetectorTest.java b/resource-providers/src/test/java/io/opentelemetry/resourceproviders/AppServerServiceNameDetectorTest.java new file mode 100644 index 000000000..776fa2cf7 --- /dev/null +++ b/resource-providers/src/test/java/io/opentelemetry/resourceproviders/AppServerServiceNameDetectorTest.java @@ -0,0 +1,132 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.resourceproviders; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class AppServerServiceNameDetectorTest { + + @Mock private AppServer appServer; + @Mock private AppServerServiceNameDetector.DirectoryTool dirTool; + @Mock private ParseBuddy parseBuddy; + + @Test + void detectNullServerClass() throws Exception { + AppServerServiceNameDetector detector = new AppServerServiceNameDetector(appServer); + assertThat(detector.detect()).isNull(); + } + + @Test + void nullDeploymentDir() throws Exception { + doReturn(AppServer.class).when(appServer).getServerClass(); + AppServerServiceNameDetector detector = new AppServerServiceNameDetector(appServer); + assertThat(detector.detect()).isNull(); + } + + @Test + void detectMissingDir() throws Exception { + Path deploymentDir = Paths.get("/fake", "location"); + + doReturn(AppServer.class).when(appServer).getServerClass(); + when(appServer.getDeploymentDir()).thenReturn(deploymentDir); + when(dirTool.isDirectory(deploymentDir)).thenReturn(false); + + AppServerServiceNameDetector detector = + new AppServerServiceNameDetector(appServer, null, dirTool); + assertThat(detector.detect()).isNull(); + } + + @Test + void detect_explodedApp() throws Exception { + Path deploymentDir = Paths.get("/fake", "location"); + Path path1 = Paths.get("path1.xml"); + Path path2 = Paths.get("something/"); + + doReturn(AppServer.class).when(appServer).getServerClass(); + when(appServer.getDeploymentDir()).thenReturn(deploymentDir); + when(appServer.isValidAppName(path1)).thenReturn(false); + when(appServer.isValidAppName(path2)).thenReturn(true); + when(dirTool.isDirectory(deploymentDir)).thenReturn(true); + when(dirTool.list(deploymentDir)).thenReturn(Stream.of(path1, path2)); + when(dirTool.isDirectory(path2)).thenReturn(true); + when(parseBuddy.handleExplodedApp(path2)).thenReturn("RadicalService99"); + + AppServerServiceNameDetector detector = + new AppServerServiceNameDetector(appServer, parseBuddy, dirTool); + assertThat(detector.detect()).isEqualTo("RadicalService99"); + } + + @Test + void detect_packagedWar() throws Exception { + Path deploymentDir = Paths.get("/fake", "location"); + Path path1 = Paths.get("meh"); + Path path2 = Paths.get("excellent.war"); + + doReturn(AppServer.class).when(appServer).getServerClass(); + when(appServer.getDeploymentDir()).thenReturn(deploymentDir); + when(appServer.isValidAppName(path1)).thenReturn(false); + when(appServer.isValidAppName(path2)).thenReturn(true); + when(dirTool.isDirectory(deploymentDir)).thenReturn(true); + when(dirTool.list(deploymentDir)).thenReturn(Stream.of(path1, path2)); + when(dirTool.isDirectory(path2)).thenReturn(false); + when(parseBuddy.handlePackagedWar(path2)).thenReturn("WhatIsItGoodFor"); + + AppServerServiceNameDetector detector = + new AppServerServiceNameDetector(appServer, parseBuddy, dirTool); + assertThat(detector.detect()).isEqualTo("WhatIsItGoodFor"); + } + + @Test + void detect_packagedEar() throws Exception { + Path deploymentDir = Paths.get("/fake", "location"); + Path path1 = Paths.get("meh"); + Path path2 = Paths.get("excellent.ear"); + + doReturn(AppServer.class).when(appServer).getServerClass(); + when(appServer.getDeploymentDir()).thenReturn(deploymentDir); + when(appServer.isValidAppName(path1)).thenReturn(false); + when(appServer.isValidAppName(path2)).thenReturn(true); + when(appServer.supportsEar()).thenReturn(true); + + when(dirTool.isDirectory(deploymentDir)).thenReturn(true); + when(dirTool.list(deploymentDir)).thenReturn(Stream.of(path1, path2)); + when(dirTool.isDirectory(path2)).thenReturn(false); + when(parseBuddy.handlePackagedEar(path2)).thenReturn("Cochlea"); + + AppServerServiceNameDetector detector = + new AppServerServiceNameDetector(appServer, parseBuddy, dirTool); + assertThat(detector.detect()).isEqualTo("Cochlea"); + } + + @Test + void detect_nothing() throws Exception { + Path deploymentDir = Paths.get("/fake", "location"); + Path path1 = Paths.get("meh"); + + doReturn(AppServer.class).when(appServer).getServerClass(); + when(appServer.getDeploymentDir()).thenReturn(deploymentDir); + when(appServer.isValidAppName(path1)).thenReturn(true); + when(appServer.supportsEar()).thenReturn(true); + + when(dirTool.isDirectory(deploymentDir)).thenReturn(true); + when(dirTool.list(deploymentDir)).thenReturn(Stream.of(path1)); + + AppServerServiceNameDetector detector = + new AppServerServiceNameDetector(appServer, parseBuddy, dirTool); + assertThat(detector.detect()).isNull(); + } +} diff --git a/resource-providers/src/test/java/io/opentelemetry/resourceproviders/ParseBuddyTest.java b/resource-providers/src/test/java/io/opentelemetry/resourceproviders/ParseBuddyTest.java new file mode 100644 index 000000000..338098fd9 --- /dev/null +++ b/resource-providers/src/test/java/io/opentelemetry/resourceproviders/ParseBuddyTest.java @@ -0,0 +1,129 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.resourceproviders; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ParseBuddyTest { + + final Path path = Paths.get("dir/"); + final Path webXml = Paths.get("dir/WEB-INF/web.xml"); + final Path applicationXml = Paths.get("dir/META-INF/application.xml"); + final InputStream webXmlStream = + new ByteArrayInputStream( + "goats".getBytes(UTF_8)); + final InputStream appXmlStream = + new ByteArrayInputStream( + "piglet".getBytes(UTF_8)); + + @Mock private AppServer appServer; + @Mock private ParseBuddy.Filesystem filesystem; + + @Test + void explodedApp_war() throws Exception { + + when(filesystem.isRegularFile(webXml)).thenReturn(true); + when(filesystem.newInputStream(webXml)).thenReturn(webXmlStream); + when(appServer.isValidResult(path, "goats")).thenReturn(true); + + ParseBuddy parseBuddy = new ParseBuddy(appServer, filesystem); + + String result = parseBuddy.handleExplodedApp(path); + assertThat(result).isEqualTo("goats"); + } + + @Test + void explodedApp_malformedWarXml() throws Exception { + InputStream stream = new ByteArrayInputStream("".getBytes(UTF_8)); + + when(filesystem.isRegularFile(webXml)).thenReturn(true); + when(filesystem.newInputStream(webXml)).thenReturn(stream); + + ParseBuddy parseBuddy = new ParseBuddy(appServer, filesystem); + + String result = parseBuddy.handleExplodedApp(path); + assertThat(result).isNull(); + } + + @Test + void explodedApp_ear() throws Exception { + + when(filesystem.isRegularFile(webXml)).thenReturn(false); + when(filesystem.isRegularFile(applicationXml)).thenReturn(true); + when(filesystem.newInputStream(applicationXml)).thenReturn(appXmlStream); + when(appServer.supportsEar()).thenReturn(true); + when(appServer.isValidResult(path, "piglet")).thenReturn(true); + + ParseBuddy parseBuddy = new ParseBuddy(appServer, filesystem); + + String result = parseBuddy.handleExplodedApp(path); + assertThat(result).isEqualTo("piglet"); + } + + @Test + void packagedWar() throws Exception { + Path warFile = Paths.get("/path/to/amaze.war"); + + ZipFile zipFile = mock(ZipFile.class); + ZipEntry zipEntry = mock(ZipEntry.class); + + when(zipFile.getEntry("WEB-INF/web.xml")).thenReturn(zipEntry); + when(appServer.isValidResult(warFile, "goats")).thenReturn(true); + when(filesystem.openZipFile(warFile)).thenReturn(zipFile); + when(zipFile.getInputStream(zipEntry)).thenReturn(webXmlStream); + + ParseBuddy parseBuddy = new ParseBuddy(appServer, filesystem); + + String result = parseBuddy.handlePackagedWar(warFile); + assertThat(result).isEqualTo("goats"); + } + + @Test + void handlePackagedThrows() throws Exception { + Path warFile = Paths.get("/path/to/amaze.war"); + + when(filesystem.openZipFile(warFile)).thenThrow(new IOException("boom")); + + ParseBuddy parseBuddy = new ParseBuddy(appServer, filesystem); + + String result = parseBuddy.handlePackagedWar(warFile); + assertThat(result).isNull(); + } + + @Test + void packagedEar() throws Exception { + Path earFile = Paths.get("/path/to/amaze.ear"); + + ZipFile zipFile = mock(ZipFile.class); + ZipEntry zipEntry = mock(ZipEntry.class); + + when(zipFile.getEntry("META-INF/application.xml")).thenReturn(zipEntry); + when(appServer.isValidResult(earFile, "piglet")).thenReturn(true); + when(filesystem.openZipFile(earFile)).thenReturn(zipFile); + when(zipFile.getInputStream(zipEntry)).thenReturn(appXmlStream); + + ParseBuddy parseBuddy = new ParseBuddy(appServer, filesystem); + + String result = parseBuddy.handlePackagedEar(earFile); + assertThat(result).isEqualTo("piglet"); + } +}