diff --git a/src/main/java/org/apache/maven/plugins/enforcer/EarModuleClasspath.java b/src/main/java/org/apache/maven/plugins/enforcer/EarModuleClasspath.java new file mode 100644 index 00000000..de9315e3 --- /dev/null +++ b/src/main/java/org/apache/maven/plugins/enforcer/EarModuleClasspath.java @@ -0,0 +1,226 @@ +package org.apache.maven.plugins.enforcer; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.Stack; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.apache.maven.enforcer.rule.api.EnforcerRule; +import org.apache.maven.enforcer.rule.api.EnforcerRuleException; +import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper; +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * This rule verifies that the EJB's Classpath entry in the manifest refers to a libray in the shared library path. + * + * The rule assumes that all EJB are located at the root of the EAR and the lib folder contains all shared libraries. + * + * @author Martin Goldhahn + * @author Stig Tore Johannesen + */ +public class EarModuleClasspath implements EnforcerRule { + + public void execute(EnforcerRuleHelper helper) throws EnforcerRuleException { + + try { + MavenProject project = (MavenProject) helper.evaluate("${project}"); + String projectType = project.getArtifact().getType(); + if (!"ear".equals(projectType)) { + throw new EnforcerRuleException("EarModuleClasspath rule can only be applied in EAR projects"); + } + File outputDir = new File(project.getBuild().getDirectory()); + String finalName = project.getBuild().getFinalName(); + File earFile = new File(outputDir, finalName + '.' + projectType); + + verifyManifests(earFile); + } catch (ExpressionEvaluationException e) { + throw new EnforcerRuleException("Unable to lookup expression " + e.getLocalizedMessage(), e); + } catch (IOException ex) { + throw new EnforcerRuleException(ex.getMessage(), ex); + } + } + + public boolean isCacheable() { + return false; + } + + public boolean isResultValid(EnforcerRule cachedRule) { + return false; + } + + public String getCacheId() { + return ""; + } + + private void verifyManifests(File earFile) throws EnforcerRuleException, IOException { + Set sharedLibs = new HashSet(); + Map> ejbClasspaths = new HashMap>(); + + ApplicationXmlHandler appXml = readApplicationXml(earFile); + + extractSharedLibsAndEjbClasspath(earFile, appXml, sharedLibs, ejbClasspaths); + + checkClassPaths(sharedLibs, ejbClasspaths, appXml); + } + + private void extractSharedLibsAndEjbClasspath(File earFile, ApplicationXmlHandler appXml, Set sharedLibs, + Map> ejbClasspaths) throws IOException { + + String libFolderName = appXml.getLibDir() + '/'; + ZipFile ear = null; + try { + ear = new ZipFile(earFile); + Enumeration entries = ear.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + String entryName = entry.getName(); + if (appXml.getEjbModules().contains(entryName)) { + Set cl = extractClassPathFromManifest(ear, entry); + ejbClasspaths.put(entryName, cl); + } else if (appXml.getWebModules().contains(entryName)) { + // web modules are ignored + } else if (entryName.startsWith(libFolderName) && entryName.endsWith(".jar")) { + // this must be a shared library + sharedLibs.add(entryName); + } + } + } finally { + if (ear != null) { + ear.close(); + } + } + } + + private Set extractClassPathFromManifest(ZipFile ear, ZipEntry entry) throws IOException { + + ZipInputStream zipStream = null; + Set classPath = new HashSet(); + try { + zipStream = new ZipInputStream(ear.getInputStream(entry)); + + ZipEntry moduleEntry; + while ((moduleEntry = zipStream.getNextEntry()) != null) { + if (moduleEntry.getName().equals(JarFile.MANIFEST_NAME)) { + ByteArrayOutputStream sb = new ByteArrayOutputStream(); + byte[] buffer = new byte[0xFFFF]; + int read; + while ((read = zipStream.read(buffer)) >= 0) { + sb.write(buffer, 0, read); + } + Manifest mf = new Manifest(new ByteArrayInputStream(sb.toByteArray())); + String cl = mf.getMainAttributes().getValue("Class-Path"); + if (cl != null) { + for (String clEntry : cl.split(" ")) { + classPath.add(clEntry); + } + } + break; + } + } + return classPath; + } finally { + if (zipStream != null) { + zipStream.close(); + } + } + } + + /** + * Read the META-INF/application.xml from the EAR. + */ + private ApplicationXmlHandler readApplicationXml(File earFile) throws IOException { + ZipFile ear = null; + try { + ear = new ZipFile(earFile); + ApplicationXmlHandler appXml = new ApplicationXmlHandler(); + ZipEntry applicationXmlEntry = ear.getEntry("META-INF/application.xml"); + SAXParser parser = SAXParserFactory.newInstance().newSAXParser(); + parser.parse(ear.getInputStream(applicationXmlEntry), appXml); + return appXml; + } catch (ParserConfigurationException e) { + throw new RuntimeException(e); + } catch (SAXException e) { + throw new RuntimeException(e); + } finally { + if (ear != null) { + ear.close(); + } + } + } + + /** + * The libraries in the classpath entry of the manifest can include the library path or not. + */ + private void checkClassPaths(Set sharedLibs, Map> ejbClasspaths, ApplicationXmlHandler appXml) throws EnforcerRuleException { + for (Map.Entry> entry : ejbClasspaths.entrySet()) { + for (String clEntry : entry.getValue()) { + if (!sharedLibs.contains(clEntry) && !sharedLibs.contains(appXml.getLibDir() + '/' + clEntry)) { + throw new EnforcerRuleException("Did not find shared library " + clEntry + " in manifest of " + entry.getKey()); + } + } + } + } + + private static class ApplicationXmlHandler extends DefaultHandler { + private Set ejbModules = new HashSet(); + private Set webModules = new HashSet(); + private String libDir; + + private Stack elementStack = new Stack(); + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + elementStack.push(qName); + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + elementStack.pop(); + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + String value = new String(ch, start, length).trim(); + + String currentElement = elementStack.peek(); + if ("ejb".equals(currentElement)) { + ejbModules.add(value); + } else if ("web-uri".equals(currentElement)) { + webModules.add(value); + } else if ("library-directory".equals(currentElement)) { + libDir = value; + } + } + + public Set getEjbModules() { + return ejbModules; + } + + public Set getWebModules() { + return webModules; + } + + public String getLibDir() { + return libDir; + } + } +} diff --git a/src/site/apt/earModuleClasspath.apt.vm b/src/site/apt/earModuleClasspath.apt.vm new file mode 100644 index 00000000..ae82b6fd --- /dev/null +++ b/src/site/apt/earModuleClasspath.apt.vm @@ -0,0 +1,126 @@ +~~ 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. + + ------ + EAR module classpath + ------ + Martin Goldhahn + ------ + April 2015 + ------ + +EAR module classpath + + This rule checks that the classpath manifest entry for Enterprise Java Bean (EJB) in an Enterprise ARchive (EAR) project refers to + existing libraries in the EAR's shared lib folder. + + The Manifest of an EJB might have a manifest entry: + <<>> + + This rule verifies that the EAR actually contains a commons-beanutils-1.8.3.jar in the EAR's lib folder. + + As the EAR's lib folder can be configured, its actual value is read from META-INF/application.xml. + This file is also read to get the list of EJBs in the EAR. + +Sample configuration: + ++---+ + + com.company + product + 1.0 + http://company/wiki/product + ear + [...] + + + com.company + ejb1 + 1.0 + ejb + + + com.company + webapp1 + 1.0 + war + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + ${enforcerApiVersion} + + + validate-ear + + enforce + + package + + + + + + + + + + ${project.groupId} + ${project.artifactId} + ${project.version} + + + + + org.apache.maven.plugins + maven-ear-plugin + + 7 + lib + product-app + + + com.oracle.jdbc7_g + + + + + + com.company + webapp1 + /webapp + + + + com.company + ejb1 + + + + + + + [...] + ++---+ + +* Trademarks + + Apache, Apache Maven, Maven and the Apache feather logo are trademarks of The Apache Software Foundation. diff --git a/src/site/apt/index.apt b/src/site/apt/index.apt index ccc4f166..09b49539 100644 --- a/src/site/apt/index.apt +++ b/src/site/apt/index.apt @@ -48,6 +48,8 @@ Extra Enforcer Rules * {{{./requireEncoding.html}requireEncoding}} - verifies that source files have a required encoding. + * {{{./earModuleClasspath.html}earModuleClasspath}} - verifies that Class-Path manifest entries in EJBs inside a EAR are valid. + [] <> diff --git a/src/site/site.xml b/src/site/site.xml index 0f1127e6..7cc5422f 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -32,6 +32,7 @@ under the License. + diff --git a/src/test/java/org/apache/maven/plugins/enforcer/EarModuleClasspathTest.java b/src/test/java/org/apache/maven/plugins/enforcer/EarModuleClasspathTest.java new file mode 100644 index 00000000..730f9f95 --- /dev/null +++ b/src/test/java/org/apache/maven/plugins/enforcer/EarModuleClasspathTest.java @@ -0,0 +1,138 @@ +package org.apache.maven.plugins.enforcer; + +import static org.hamcrest.CoreMatchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.FileNotFoundException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipException; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.enforcer.rule.api.EnforcerRuleException; +import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper; +import org.apache.maven.model.Build; +import org.apache.maven.project.MavenProject; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class EarModuleClasspathTest { + + @Rule + public ExpectedException exception = ExpectedException.none(); + + private EnforcerRuleHelper helper; + private MavenProject project; + private Artifact artifact; + private Build build; + private EarModuleClasspath rule; + private static int javaVersion; + + @BeforeClass + public static void initClass() { + javaVersion = javaMinor(); + } + + @Before + public void initFields() { + helper = mock(EnforcerRuleHelper.class); + project = mock(MavenProject.class); + artifact = mock(Artifact.class); + build = mock(Build.class); + rule = new EarModuleClasspath(); + } + + @Test + public void testExecuteWithWrongProjectType() throws Exception { + + when(helper.evaluate("${project}")).thenReturn(project); + when(project.getArtifact()).thenReturn(artifact); + when(artifact.getType()).thenReturn("jar"); + + exception.expectMessage("EarModuleClasspath rule can only be applied in EAR projects"); + rule.execute(helper); + } + + @Test + public void testWithMissingEar() throws Exception { + + when(helper.evaluate("${project}")).thenReturn(project); + when(project.getArtifact()).thenReturn(artifact); + when(artifact.getType()).thenReturn("ear"); + when(project.getBuild()).thenReturn(build); + when(build.getDirectory()).thenReturn("target/test-classes"); + when(build.getFinalName()).thenReturn("missing-ear"); + + exception.expect(EnforcerRuleException.class); + // java 1.5 and 1.6 throw a ZipException instead of a FileNotFoundException + if (javaVersion > 6) { + exception.expectCause(isA(FileNotFoundException.class)); + } else { + exception.expectCause(isA(ZipException.class)); + } + rule.execute(helper); + } + + /** + * This EAR file has no classpath in hte EJB's manifest. + * The test should succeed. + */ + @Test + public void testEarWithoutLibFolder() throws Exception { + when(helper.evaluate("${project}")).thenReturn(project); + when(project.getArtifact()).thenReturn(artifact); + when(artifact.getType()).thenReturn("ear"); + when(project.getBuild()).thenReturn(build); + when(build.getDirectory()).thenReturn("target/test-classes"); + when(build.getFinalName()).thenReturn("test-ear-no-lib"); + + rule.execute(helper); + } + + /** + * This EAR's EJB have a Class-Path manifest entry + * @throws Exception + */ + @Test + public void testEarWithLibInClasspath() throws Exception { + when(helper.evaluate("${project}")).thenReturn(project); + when(project.getArtifact()).thenReturn(artifact); + when(artifact.getType()).thenReturn("ear"); + when(project.getBuild()).thenReturn(build); + when(build.getDirectory()).thenReturn("target/test-classes"); + when(build.getFinalName()).thenReturn("test-ear-lib-in-classpath"); + + rule.execute(helper); + } + + /** + * This EAR's EJB have a Class-Path manifest entry + * @throws Exception + */ + @Test + public void testEarNoLibInClasspath() throws Exception { + when(helper.evaluate("${project}")).thenReturn(project); + when(project.getArtifact()).thenReturn(artifact); + when(artifact.getType()).thenReturn("ear"); + when(project.getBuild()).thenReturn(build); + when(build.getDirectory()).thenReturn("target/test-classes"); + when(build.getFinalName()).thenReturn("test-ear-no-lib-in-classpath"); + + rule.execute(helper); + } + + private static int javaMinor() { + String specString = System.getProperty("java.specification.version"); + Pattern versionPattern = Pattern.compile("\\d+\\.(\\d+)"); + Matcher m = versionPattern.matcher(specString); + if (m.matches()) { + return Integer.valueOf(m.group(1)); + } + + throw new IllegalStateException("Cannot parse Java version"); + } +} diff --git a/src/test/resources/test-ear-lib-in-classpath.ear b/src/test/resources/test-ear-lib-in-classpath.ear new file mode 100644 index 00000000..a5ecd8b5 Binary files /dev/null and b/src/test/resources/test-ear-lib-in-classpath.ear differ diff --git a/src/test/resources/test-ear-no-lib-in-classpath.ear b/src/test/resources/test-ear-no-lib-in-classpath.ear new file mode 100644 index 00000000..ea490f10 Binary files /dev/null and b/src/test/resources/test-ear-no-lib-in-classpath.ear differ diff --git a/src/test/resources/test-ear-no-lib.ear b/src/test/resources/test-ear-no-lib.ear new file mode 100644 index 00000000..58104778 Binary files /dev/null and b/src/test/resources/test-ear-no-lib.ear differ