/* * Copyright 2008 Alin Dreghiciu, Achim Nierbeck. * * Licensed 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.ops4j.pax.url.war.internal; import java.io.IOException; import java.io.InputStream; import java.net.JarURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.Properties; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.ops4j.lang.NullArgumentException; import org.ops4j.lang.PreConditionException; import org.ops4j.net.URLUtils; import org.ops4j.pax.swissbox.bnd.BndUtils; import org.ops4j.pax.swissbox.bnd.OverwriteMode; import org.ops4j.pax.url.war.ServiceConstants; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; /** * Abstract url connection for wrap protocol handler. * Subclasses must provide the processing instructions. * * @author Alin Dreghiciu, Achim Nierbeck * @since 0.1.0, January 14, 2008 */ abstract class AbstractConnection extends URLConnection { /** * Service configuration. */ private final Configuration m_configuration; /** * DocumentBuilderFactory for parsing web.xml files */ private static DocumentBuilderFactory dbf = null; /** * The pattern blacklist to verify that the jar is "legal" within a web-context. */ private static final Pattern[] blacklist = { Pattern.compile("servlet\\.jar"), Pattern.compile("servlet-[0-9]+(\\.[0-9])+\\.jar"), Pattern.compile("servlet-api\\.jar"), Pattern.compile("servlet-api-[0-9]+(\\.[0-9])+\\.jar"), Pattern.compile("jasper\\.jar"), Pattern.compile("jasper-[0-9]+(\\.[0-9])+\\.jar"), Pattern.compile("jsp-api\\.jar"), Pattern.compile("jsp-api-[0-9]+(\\.[0-9])+\\.jar")}; /** * Creates a new connection. * * @param url url to be handled; cannot be null. * @param configuration protocol configuration; cannot be null * * @throws MalformedURLException if url path is empty * @throws NullArgumentException if url or configuration is null */ protected AbstractConnection( final URL url, final Configuration configuration ) throws MalformedURLException { super( url ); NullArgumentException.validateNotNull( url, "URL" ); NullArgumentException.validateNotNull( configuration, "Configuration" ); final String path = url.getPath(); if( path == null || path.trim().length() == 0 ) { throw new MalformedURLException( "Path cannot empty" ); } m_configuration = configuration; } /** * Returns the input stream denoted by the url. * * @return the input stream for the resource denoted by url * * @throws java.io.IOException in case of an exception during accessing the resource * @see java.net.URLConnection#getInputStream() */ @Override public InputStream getInputStream() throws IOException { connect(); final Properties instructions = getInstructions(); PreConditionException.validateNotNull( instructions, "Instructions" ); // the instructions must always contain the war file final String warUri = instructions.getProperty( ServiceConstants.INSTR_WAR_URL ); if( warUri == null || warUri.trim().length() == 0 ) { throw new IOException( "Instructions file must contain a property named " + ServiceConstants.INSTR_WAR_URL ); } generateClassPathInstruction( instructions ); generateImportPackageFromWebXML( instructions ); return createBundle( URLUtils.prepareInputStream(new URL(warUri), !m_configuration.getCertificateCheck()), instructions, warUri ); } /** * Actually create the bundle based on the parsed instructions and the given stream * @param warUri * @param instructions * @return * @throws IOException */ protected InputStream createBundle(InputStream inputStream, Properties instructions, String warUri) throws IOException { return BndUtils.createBundle( inputStream, instructions, warUri ); } /** * Actually create the bundle based on the parsed instructions and the given stream * @param inputStream * @param instructions * @param warUri * @param overwriteMode * @return an input stream for the generated bundle * @throws IOException * * @see BndUtils.createBundle */ protected InputStream createBundle(InputStream inputStream, Properties instructions, String warUri, OverwriteMode overwriteMode) throws IOException { return BndUtils.createBundle( inputStream, instructions, warUri, overwriteMode ); } /** * Returns the processing instructions. * * @return processing instructions * * @throws java.io.IOException if instructions file can not be returned */ protected abstract Properties getInstructions() throws IOException; /** * Getter. * * @return configuration */ protected Configuration getConfiguration() { return m_configuration; } /** * Generates the Bundle-ClassPath header by merging the Original classpath with:<br/> * .<br/> * WEB-INF/classes<br/> * all jars found in WEB-INF/lib * * @param instructions instructions * * @throws java.io.IOException re-thrown from extractJarListFromWar() */ private static void generateClassPathInstruction( final Properties instructions ) throws IOException { final List<String> bundleClassPath = new ArrayList<String>(); // first take the bundle class path if present bundleClassPath.addAll( toList( instructions.getProperty( ServiceConstants.INSTR_BUNDLE_CLASSPATH ), "," ) ); // then get the list of jars in WEB-INF/lib bundleClassPath.addAll( extractJarListFromWar( instructions.getProperty( ServiceConstants.INSTR_WAR_URL ) ) ); // check if we have a "WEB-INF/classpath" entry if( !bundleClassPath.contains( "WEB-INF/classes" ) ) { bundleClassPath.add( 0, "WEB-INF/classes" ); } // check if we have a "." entry /* War archives do have the required classes at WEB-INF/classes "." is not allowed if( !bundleClassPath.contains( "." ) ) { bundleClassPath.add( 0, "." ); } */ // set back the new bundle classpath instructions.setProperty( ServiceConstants.INSTR_BUNDLE_CLASSPATH, join( bundleClassPath, "," ) ); } /** * Adds Package-Import for classes contained in the web.xml of the war. * * @param instructions - Properties containing the instructions for the manifest generation * @throws IOException */ private static void generateImportPackageFromWebXML(Properties instructions) throws IOException { String warUri = instructions.getProperty( ServiceConstants.INSTR_WAR_URL ); JarFile jarFile = null; List<String> webXmlImports = new ArrayList<String>(); try { final JarURLConnection conn = (JarURLConnection) new URL( "jar:" + warUri + "!/" ).openConnection(); conn.setUseCaches( false ); jarFile = conn.getJarFile(); Enumeration<JarEntry> entries = jarFile.entries(); while( entries.hasMoreElements() ) { JarEntry entry = (JarEntry) entries.nextElement(); if ("WEB-INF/web.xml".equalsIgnoreCase(entry.getName())){ //Found the web.xml will try to get all "-class" attributes from it to import them if (dbf == null) { dbf = DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware(true); dbf.setValidating(false); dbf.setAttribute("http://xml.org/sax/features/namespaces", true); dbf.setAttribute("http://xml.org/sax/features/validation", false); dbf.setAttribute("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); dbf.setAttribute("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); } DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(jarFile.getInputStream(entry)); NodeList childNodes = doc.getDocumentElement().getChildNodes(); parseChildNodes(webXmlImports, childNodes); break; } } StringBuffer buff = new StringBuffer(instructions.getProperty("Import-Package")); for (String importPackage : webXmlImports) { if (buff.toString().contains(importPackage)) continue; //skip this one it's already included buff.append(","); buff.append(importPackage); buff.append(";resolution:=optional"); } instructions.setProperty("Import-Package", buff.toString()); } catch( ClassCastException e ) { throw new IOException( "Provided url [" + warUri + "] does not refer a valid war file", e ); } catch (MalformedURLException e) { throw new IOException( "Provided url [" + warUri + "] does not refer a valid war file", e ); } catch (IOException e) { throw new IOException( "Provided url [" + warUri + "] does not refer a valid war file", e ); } catch (ParserConfigurationException e) { throw new IOException( "Provided url [" + warUri + "] does not refer a valid war file", e ); } catch (SAXException e) { throw new IOException( "Provided url [" + warUri + "] does not refer a valid war file", e ); } finally { if( jarFile != null ) { try { jarFile.close(); } catch( IOException ignore ) { // ignore } } } } /** * @param webXmlImports * @param childNodes */ private static void parseChildNodes(List<String> webXmlImports, NodeList childNodes) { for (int i = 0; i < childNodes.getLength(); i++) { Node node = childNodes.item(i); NodeList subNodes = node.getChildNodes(); if (subNodes != null) parseChildNodes(webXmlImports, subNodes); String nodeName = node.getNodeName(); if (nodeName.contains("-class")) { //found a class attribute extract package String lookupClass = node.getTextContent(); String packageName = lookupClass.substring(0, lookupClass.lastIndexOf(".")).trim(); webXmlImports.add(packageName); } } } /** * Does nothing. */ @Override public void connect() { // do nothing } /** * Extracts the list of jars from a war file. The list will contain all jars under WEB-INF/lib directory. * * @param warUri war file uri * * @return list of jars * * @throws java.io.IOException re-thrown from accessing urls or if the warUri does not refer to a jar */ private static List<String> extractJarListFromWar( final String warUri ) throws IOException { final List<String> list = new ArrayList<String>(); JarFile jarFile = null; try { final JarURLConnection conn = (JarURLConnection) new URL( "jar:" + warUri + "!/" ).openConnection(); conn.setUseCaches( false ); jarFile = conn.getJarFile(); Enumeration<JarEntry> entries = jarFile.entries(); while( entries.hasMoreElements() ) { JarEntry entry = (JarEntry) entries.nextElement(); String name = entry.getName(); if( !name.startsWith( "WEB-INF/lib/" ) ) { continue; } if( !name.endsWith( ".jar" ) ) { continue; } if ( !checkJarIsLegal(name) ) { continue; } list.add( name ); } } catch( ClassCastException e ) { throw new IOException( "Provided url [" + warUri + "] does not refer a valid war file" ); } finally { if( jarFile != null ) { try { jarFile.close(); } catch( IOException ignore ) { // ignore } } } return list; } /** * verifies that the given jar name is not contained * in the blacklist. * * @param name of the jar which needs verification * @return * true - if the jar is a legal jar </br> * false - if the jar is not supposed to be in a war archive like servlet.jar */ protected static boolean checkJarIsLegal(String name) { boolean isMatched = false; for (Pattern pattern : blacklist) { isMatched = pattern.matcher(name).find(); if (isMatched) { break; } } return !isMatched; } /** * Splits a delimiter separated string into a list. * * @param separatedString string to be split * @param delimiter delimiter * * @return list composed out of the string segments */ protected static List<String> toList( final String separatedString, final String delimiter ) { final List<String> list = new ArrayList<String>(); if( separatedString != null ) { list.addAll( Arrays.asList( separatedString.split( delimiter ) ) ); } return list; } /** * Joins elements from a collection into a delimiter separated string. * * @param strings collection of ellements * @param delimiter delimiter * * @return string composed from the collection elements delimited by the delimiter */ protected static String join( final Collection<String> strings, final String delimiter ) { final StringBuffer buffer = new StringBuffer(); final Iterator<String> iter = strings.iterator(); while( iter.hasNext() ) { buffer.append( iter.next() ); if( iter.hasNext() ) { buffer.append( delimiter ); } } return buffer.toString(); } }