/** * Copyright (c) 2000-present Liferay, Inc. All rights reserved. * * This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. */ package com.liferay.ant.bnd.jsp; import aQute.bnd.header.Attrs; import aQute.bnd.header.OSGiHeader; import aQute.bnd.header.Parameters; import aQute.bnd.osgi.Analyzer; import aQute.bnd.osgi.Clazz; import aQute.bnd.osgi.Constants; import aQute.bnd.osgi.Descriptors; import aQute.bnd.osgi.Descriptors.PackageRef; import aQute.bnd.osgi.Domain; import aQute.bnd.osgi.Instruction; import aQute.bnd.osgi.Instructions; import aQute.bnd.osgi.Jar; import aQute.bnd.osgi.Packages; import aQute.bnd.osgi.Resource; import aQute.bnd.service.AnalyzerPlugin; import aQute.lib.env.Header; import aQute.lib.io.IO; import aQute.lib.strings.Strings; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.xml.sax.Attributes; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.DefaultHandler; /** * @author Raymond Augé */ public class JspAnalyzerPlugin implements AnalyzerPlugin { @Override public boolean analyzeJar(Analyzer analyzer) throws Exception { addManifestPackageImports(analyzer); Parameters parameters = OSGiHeader.parseHeader( analyzer.getProperty("-jsp")); if (parameters.isEmpty()) { return false; } Instructions instructions = new Instructions(parameters); boolean matches = false; Jar jar = analyzer.getJar(); Map<String, Resource> resources = jar.getResources(); Set<String> keys = new HashSet<String>(resources.keySet()); Set<String> taglibURIs = new HashSet<>(); for (String key : keys) { for (Instruction instruction : instructions.keySet()) { if (instruction.matches(key)) { if (instruction.isNegated()) { break; } Resource resource = jar.getResource(key); String jsp = IO.collect( resource.openInputStream(), "UTF-8"); addApiUses(analyzer, jsp); addTaglibRequirements(analyzer, jsp, taglibURIs); matches = true; } } } if (matches) { addRequiredPackageImports(analyzer, _REQUIRED_PACKAGE_NAMES); } return false; } protected void addApiUses(Analyzer analyzer, String content) { int contentX = -1; int contentY = content.length(); while (true) { contentX = content.lastIndexOf("<%@", contentY); if (contentX == -1) { break; } contentY = contentX; int importX = content.indexOf("import=\"", contentY); int importY = -1; if (importX != -1) { importX = importX + "import=\"".length(); importY = content.indexOf("\"", importX); } if ((importX != -1) && (importY != -1)) { String contentFragment = content.substring(importX, importY); String[] packageFragments = contentFragment.split("\\s*,\\s*"); for (String packageFragment : packageFragments) { int index = packageFragment.lastIndexOf('.'); Matcher matcher = _staticImportPattern.matcher( packageFragment); if (matcher.matches()) { packageFragment = matcher.group("package"); packageFragment = packageFragment.substring(0, packageFragment.length() - 1); index = packageFragment.length(); } if (index != -1) { Packages packages = analyzer.getReferred(); String packageName = packageFragment.substring(0, index); PackageRef packageRef = analyzer.getPackageRef(packageName); packages.put(packageRef, new Attrs()); addApiUses(analyzer, packageFragment, packageRef); } } } contentY -= 3; } } protected void addApiUses( Analyzer analyzer, String content, PackageRef packageRef) { for (Jar jar : analyzer.getClasspath()) { addJarApiUses(analyzer, content, packageRef, jar); } } protected void addJarApiUses( Analyzer analyzer, String content, PackageRef packageRef, Jar jar) { Map<String, Map<String, Resource>> resourceMaps = jar.getDirectories(); Map<String, Resource> resourceMap = resourceMaps.get( packageRef.getPath()); if ((resourceMap == null) || resourceMap.isEmpty()) { return; } if (content.endsWith("*")) { for (Entry<String, Resource> entry : resourceMap.entrySet()) { String key = entry.getKey(); if (!key.endsWith(".class")) { continue; } addResourceApiUses(analyzer, key, entry.getValue()); } } else { String fqnToPath = Descriptors.fqnToPath(content); if (resourceMap.containsKey(fqnToPath)) { Resource resource = resourceMap.get(fqnToPath); addResourceApiUses(analyzer, content, resource); } } } protected void addManifestPackageImports(Analyzer analyzer) { Packages packages = analyzer.getClasspathExports(); for (Jar jar : analyzer.getClasspath()) { try { Manifest manifest = jar.getManifest(); if (manifest == null) { continue; } Domain domain = Domain.domain(manifest); Parameters parameters = domain.getExportPackage(); for (Entry<String, Attrs> entry : parameters.entrySet()) { PackageRef packageRef = analyzer.getPackageRef( entry.getKey()); Attrs attrs = packages.get(packageRef); if (attrs.isEmpty()) { packages.put(packageRef, entry.getValue()); } } } catch (Exception e) { } } } protected void addRequiredPackageImports( Analyzer analyzer, String[] packageNames) { Packages packages = analyzer.getReferred(); for (String packageName : packageNames) { PackageRef packageRef = analyzer.getPackageRef(packageName); Matcher matcher = _packagePattern.matcher(packageRef.getFQN()); if (matcher.matches() && !packages.containsKey(packageRef)) { packages.put(packageRef, new Attrs()); } } } protected void addResourceApiUses( Analyzer analyzer, String fqnToPath, Resource resource) { Clazz clazz = null; try { InputStream inputStream = resource.openInputStream(); clazz = new Clazz(analyzer, fqnToPath, resource); try { clazz.parseClassFile(); } finally { inputStream.close(); } } catch (Throwable e) { return; } Set<PackageRef> packageRefs = clazz.getAPIUses(); for (PackageRef packageRef : packageRefs) { Packages packages = analyzer.getReferred(); packages.put(packageRef, new Attrs()); } } protected void addTaglibRequirement( Set<String> taglibRequirements, String uri) { Parameters parameters = new Parameters(); Attrs attrs = new Attrs(); attrs.put( Constants.FILTER_DIRECTIVE, "\"(&(osgi.extender=jsp.taglib)(uri=" + uri + "))\""); parameters.put("osgi.extender", attrs); taglibRequirements.add(parameters.toString()); } protected void addTaglibRequirements( Analyzer analyzer, String content, Set<String> taglibURIs) { Set<String> taglibRequirements = new TreeSet<>(); for (String uri : getTaglibURIs(content)) { if (taglibURIs.contains(uri)) { continue; } taglibURIs.add(uri); // Check to see if the JAR provides this TLD itself which would // indicate that it already has access to the required classes if (containsTLD(analyzer, analyzer.getJar(), "META-INF", uri) || containsTLD(analyzer, analyzer.getJar(), "WEB-INF/tld", uri) || containsTLDInBundleClassPath(analyzer, "META-INF", uri)) { continue; } if (Arrays.binarySearch(_JSTL_CORE_URIS, uri) < 0) { addTaglibRequirement(taglibRequirements, uri); } } if (taglibRequirements.isEmpty()) { return; } String value = analyzer.getProperty(Constants.REQUIRE_CAPABILITY); if (value != null) { Parameters parameters = OSGiHeader.parseHeader(value); for (Entry<String, Attrs> entry : parameters.entrySet()) { String key = Header.removeDuplicateMarker(entry.getKey()); StringBuilder sb = new StringBuilder(key); Attrs attrs = entry.getValue(); if (attrs != null) { sb.append(";"); attrs.append(sb); } taglibRequirements.add(sb.toString()); } } analyzer.setProperty( Constants.REQUIRE_CAPABILITY, Strings.join(taglibRequirements)); } protected Set<String> getTaglibURIs(String originalContent) { String content = originalContent.replaceAll("<%--[\\s\\S]*?--%>",""); int contentX = -1; int contentY = content.length(); Set<String> taglibURis = new HashSet<String>(); while (true) { contentX = content.lastIndexOf("<%@", contentY); if (contentX == -1) { break; } contentY = contentX; int importX = content.indexOf("uri=\"", contentY); int importY = -1; if (importX != -1) { importX = importX + "uri=\"".length(); importY = content.indexOf("\"", importX); } if ((importX != -1) && (importY != -1)) { String s = content.substring(importX, importY); taglibURis.add(s); } contentY -= 3; } return taglibURis; } protected boolean containsTLD( Analyzer analyzer, Jar jar, String root, String uri) { Map<String, Map<String, Resource>> resourceMaps = jar.getDirectories(); Map<String, Resource> resourceMap = resourceMaps.get(root); if (resourceMap == null || resourceMap.isEmpty()) { Resource resource = jar.getResource(root); if ((resource != null) && matchesURI(analyzer, root, resource, uri)) { return true; } return false; } for (Entry<String, Resource> entry : resourceMap.entrySet()) { String path = entry.getKey(); Resource resource = entry.getValue(); Matcher matcher = _tldPattern.matcher(path); if (matcher.matches() && matchesURI(analyzer, path, resource, uri)) { return true; } } return false; } protected boolean containsTLDInBundleClassPath( Analyzer analyzer, String root, String uri) { Parameters parameters = new Parameters( analyzer.getProperty(Constants.BUNDLE_CLASSPATH)); if (parameters.isEmpty()) { return false; } Jar jar = analyzer.getJar(); for (String entry : parameters.keySet()) { String entryLowerCase = entry.toLowerCase(); if (!entryLowerCase.endsWith(".jar") && !entryLowerCase.endsWith(".zip")) { continue; } Resource resource = jar.getResource(entry); if (resource == null) { continue; } try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()){ resource.write(byteArrayOutputStream); try (InputStream inputStream = new ByteArrayInputStream( byteArrayOutputStream.toByteArray())) { Jar classPathJar = new Jar(entry, inputStream); if (containsTLD(analyzer, classPathJar, root, uri)) { return true; } } } catch (Exception e) { continue; } } return false; } protected boolean matchesURI( Analyzer analyzer, String path, Resource resource, final String uri) { try { URIFinder uriFinder = new URIFinder(uri); SAXParser saxParser = _saxParserFactory.newSAXParser(); XMLReader xmlReader = saxParser.getXMLReader(); xmlReader.setContentHandler(uriFinder); xmlReader.setFeature(_LOAD_EXTERNAL_DTD, false); xmlReader.setEntityResolver(new NullEntityResolver()); xmlReader.parse(new InputSource(resource.openInputStream())); return uriFinder.hasURI(); } catch (Exception e) { analyzer.error( "Unexpected exception in processing TLD " + path + ": " + e); } return false; } private static final String[] _JSTL_CORE_URIS = new String[] { "http://java.sun.com/jsp/jstl/core", "http://java.sun.com/jsp/jstl/fmt", "http://java.sun.com/jsp/jstl/functions", "http://java.sun.com/jsp/jstl/sql", "http://java.sun.com/jsp/jstl/xml" }; private static final String _LOAD_EXTERNAL_DTD = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; private static final String[] _REQUIRED_PACKAGE_NAMES = new String[] { "javax.servlet", "javax.servlet.http" }; private static final Pattern _packagePattern = Pattern.compile( "[_A-Za-z$][_A-Za-z0-9$]*(\\.[_A-Za-z$][_A-Za-z0-9$]*)*"); private static final Pattern _staticImportPattern = Pattern.compile( "\\s*static\\s+((?<package>(\\p{javaJavaIdentifierStart}" + "\\p{javaJavaIdentifierPart}*\\.)+)(\\p{javaJavaIdentifierStart}" + "\\p{javaJavaIdentifierPart}*\\.)" + "(\\*|(\\p{javaJavaIdentifierStart}" + "\\p{javaJavaIdentifierPart}*)))\\s*"); private static final Pattern _tldPattern = Pattern.compile(".*\\.tld"); private final SAXParserFactory _saxParserFactory = SAXParserFactory.newInstance(); private class NullEntityResolver implements EntityResolver { @Override public InputSource resolveEntity( String publicId, String systemId) throws SAXException, IOException { return new InputSource(); } } private class URIFinder extends DefaultHandler { public URIFinder(String uri) { _uri = uri; } @Override public void startElement( String uri, String localName, String qName, Attributes attributes) throws SAXException { if (qName.equals("uri")) { _inURI = true; } } @Override public void characters(char[] chars, int start, int length) throws SAXException { if (!_inURI) { return; } String value = new String(chars, start, length); _hasURI = _uri.equals(value.replaceAll("^\\s*(.*)\\s*$", "$1")); _inURI = false; } public boolean hasURI() { return _hasURI; } private boolean _hasURI; private boolean _inURI; private String _uri; } }