blob: 5d50c424131eca7f451655220c603202b900acd8 [file] [log] [blame]
/*
* 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.
*/
// See #13931.
package org.netbeans.nbbuild;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import javax.help.HelpSet;
import javax.help.HelpSetException;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.FileScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Mapper;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/** Task to check various aspects of JavaHelp helpsets.
* <ol>
* <li>General parsability as far as JavaHelp is concerned.
* <li>Map IDs are not duplicated.
* <li>Map IDs point to real HTML files (and anchors where specified).
* <li>TOC/Index navigators refer to real map IDs.
* <li>HTML links in reachable HTML files point to valid places (including anchors).
* </ol>
* @author Jesse Glick, Marek Slama
*/
public class CheckHelpSetsBin extends Task {
private List<FileSet> filesets = new ArrayList<>();
private Set<String> excludedModulesSet;
/**
* Adds a set of module JARs. Class-Path extensions (including separate doc JARs) should not be included.
*/
public void addFileset(FileSet fs) {
filesets.add(fs);
}
private URLClassLoader createGlobalClassLoader(File dir, String[] files) throws MalformedURLException {
URL[] globalClassPath = new URL[files.length];
for (int i = 0; i < files.length; i++) {
globalClassPath[i] = new File(dir, files[i]).toURI().toURL();
}
return new URLClassLoader(globalClassPath, ClassLoader.getSystemClassLoader().getParent(), new NbDocsStreamHandler.Factory());
}
private Map<String,URLClassLoader> createClassLoaderMap(File dir, String[] files) throws IOException {
Map<String,URLClassLoader> m = new TreeMap<>();
for (int i = 0; i < files.length; i++) {
File moduleJar = new File(dir, files[i]);
Manifest manifest;
try (JarFile jar = new JarFile(moduleJar)) {
manifest = jar.getManifest();
}
if (manifest == null) {
log(moduleJar + " has no manifest", Project.MSG_WARN);
continue;
}
String codename = JarWithModuleAttributes.extractCodeName(manifest.getMainAttributes());
if (codename == null) {
log(moduleJar + " is not a module", Project.MSG_WARN);
continue;
}
m.put(codename.replaceFirst("/[0-9]+$", ""), new URLClassLoader(new URL[] {moduleJar.toURI().toURL()}, ClassLoader.getSystemClassLoader().getParent(), new NbDocsStreamHandler.Factory()));
}
return m;
}
private Set<String> parseExcludeModulesProperty (String prop) {
if (prop == null) {
return Collections.emptySet();
}
return new HashSet<>(Arrays.asList(prop.split(",")));
}
public @Override void execute() throws BuildException {
String p = getProject().getProperty("javahelpbin.exclude.modules");
excludedModulesSet = parseExcludeModulesProperty(p);
for (FileSet fs : filesets) {
FileScanner scanner = fs.getDirectoryScanner(getProject());
File dir = scanner.getBasedir();
String[] files = scanner.getIncludedFiles();
URLClassLoader globalClassLoader;
Map<String,URLClassLoader> classLoaderMap;
try {
globalClassLoader = createGlobalClassLoader(dir, files);
classLoaderMap = createClassLoaderMap(dir, files);
NbDocsStreamHandler.NbDocsURLConnection.globalClassLoader.set(globalClassLoader);
NbDocsStreamHandler.NbDocsURLConnection.classLoaderMap.set(classLoaderMap);
CheckLinks.handlerFactory.set(new NbDocsStreamHandler.Factory());
for (Map.Entry<String,URLClassLoader> entry : classLoaderMap.entrySet()) {
String cnb = entry.getKey();
if (excludedModulesSet.contains(cnb)) {
log("skipping module: " + cnb, Project.MSG_INFO);
continue;
}
URLClassLoader l = entry.getValue();
Manifest m;
InputStream is = l.getResourceAsStream("META-INF/MANIFEST.MF");
if (is != null) {
try {
m = new Manifest(is);
} finally {
is.close();
}
} else {
log("No manifest in " + Arrays.toString(l.getURLs()), Project.MSG_WARN);
continue;
}
for (String resource : new String[] {m.getMainAttributes().getValue("OpenIDE-Module-Layer"), "META-INF/generated-layer.xml"}) {
if (resource == null) {
continue;
}
URL layer = l.getResource(resource);
if (layer == null) {
log("No layer " + resource, Project.MSG_VERBOSE);
continue;
}
Document doc;
try {
doc = XMLUtil.parse(new InputSource(layer.toString()), false, false, XMLUtil.rethrowHandler(), XMLUtil.nullResolver());
} catch (SAXException x) {
log("Could not parse " + layer, x, Project.MSG_WARN);
continue;
}
for (Element services : XMLUtil.findSubElements(doc.getDocumentElement())) {
if (!services.getTagName().equals("folder") || !services.getAttribute("name").equals("Services")) {
continue;
}
for (Element javahelp : XMLUtil.findSubElements(services)) {
if (!javahelp.getTagName().equals("folder") || !javahelp.getAttribute("name").equals("JavaHelp")) {
continue;
}
JAVAHELP: for (Element registration : XMLUtil.findSubElements(javahelp)) {
if (!registration.getTagName().equals("file")) {
continue;
}
InputSource input = null;
String url = registration.getAttribute("url");
if (!url.isEmpty()) {
input = new InputSource(new URL(layer, url).toString());
} else {
NodeList nl = registration.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
if (nl.item(i).getNodeType() == Node.CDATA_SECTION_NODE) {
if (input == null) {
input = new InputSource(new StringReader(nl.item(i).getNodeValue()));
} else {
log("Multiple content for " + registration.getAttribute("name") + " in " + layer, Project.MSG_WARN);
continue JAVAHELP;
}
}
}
if (input == null) {
log("No content for " + registration.getAttribute("name") + " in " + layer, Project.MSG_WARN);
}
}
Document doc2;
try {
doc2 = XMLUtil.parse(input, false, false, XMLUtil.rethrowHandler(), XMLUtil.nullResolver());
} catch (SAXException x) {
log("Could not parse " + registration.getAttribute("name") + " in " + layer, x, Project.MSG_WARN);
continue;
}
URI helpsetref = URI.create(doc2.getDocumentElement().getAttribute("url"));
if ("nbdocs".equals(helpsetref.getScheme()) && helpsetref.getAuthority() == null) {
try {
helpsetref = new URI(helpsetref.getScheme(), cnb, helpsetref.getPath(), helpsetref.getQuery(), helpsetref.getFragment());
} catch (URISyntaxException x) {
throw new BuildException(x);
}
}
log("checking: " + helpsetref, Project.MSG_INFO);
checkHelpSetURL(CheckLinks.toURL(helpsetref), globalClassLoader, l, classLoaderMap, cnb);
}
}
}
}
}
} catch (IOException x) {
throw new BuildException(x);
} finally {
NbDocsStreamHandler.NbDocsURLConnection.globalClassLoader.set(null);
NbDocsStreamHandler.NbDocsURLConnection.classLoaderMap.set(null);
CheckLinks.handlerFactory.set(null);
}
}
}
private void checkHelpSetURL
(URL hsURL, ClassLoader globalClassLoader, ClassLoader moduleClassLoader, Map<String,URLClassLoader> classLoaderMap, String cnb) {
HelpSet hs = null;
try {
hs = new HelpSet(moduleClassLoader, hsURL);
} catch (HelpSetException ex) {
throw new BuildException("Failed to parse " + hsURL + ": " + ex, ex, getLocation());
}
javax.help.Map map = hs.getCombinedMap();
Enumeration<?> e = map.getAllIDs();
Set<URI> okurls = new HashSet<>(1000);
Set<URI> badurls = new HashSet<>(1000);
Set<URI> cleanurls = new HashSet<>(1000);
while (e.hasMoreElements()) {
javax.help.Map.ID id = (javax.help.Map.ID)e.nextElement();
URL u = null;
try {
u = id.getURL();
} catch (MalformedURLException ex) {
log("id:" + id, Project.MSG_WARN);
ex.printStackTrace();
}
if (u == null) {
throw new BuildException("Bogus map ID: " + id.id + " in: " + cnb);
}
log("Checking ID " + id.id, Project.MSG_VERBOSE);
try {
//System.out.println("CALL OF CheckLinks.scan");
List<String> errors = new ArrayList<>();
CheckLinks.scan(this, globalClassLoader, classLoaderMap, id.id, "",
new URI(u.toExternalForm()), okurls, badurls, cleanurls, false, false, false, 2,
Collections.<Mapper>emptyList(), errors);
for (String error : errors) {
log(error, Project.MSG_WARN);
}
//System.out.println("RETURN OF CheckLinks.scan");
} catch (URISyntaxException | IOException ex) {
ex.printStackTrace();
}
}
}
/* Unused:
private void checkHelpSet(File hsfile) throws Exception {
log("Checking helpset: " + hsfile);
HelpSet hs = new HelpSet(null, hsfile.toURI().toURL());
javax.help.Map map = hs.getCombinedMap();
log("Parsed helpset, checking map IDs in TOC/Index navigators...");
NavigatorView[] navs = hs.getNavigatorViews();
for (int i = 0; i < navs.length; i++) {
String name = navs[i].getName();
File navfile = new File(hsfile.getParentFile(), (String)navs[i].getParameters().get("data"));
if (! navfile.exists()) throw new BuildException("Navigator " + name + " not found", new Location(navfile.getAbsolutePath()));
if (navs[i] instanceof IndexView) {
log("Checking index navigator " + name, Project.MSG_VERBOSE);
IndexView.parse(navfile.toURI().toURL(), hs, Locale.getDefault(), new VerifyTIFactory(hs, map, navfile, false));
} else if (navs[i] instanceof TOCView) {
log("Checking TOC navigator " + name, Project.MSG_VERBOSE);
TOCView.parse(navfile.toURI().toURL(), hs, Locale.getDefault(), new VerifyTIFactory(hs, map, navfile, true));
} else {
log("Skipping non-TOC/Index view: " + name, Project.MSG_VERBOSE);
}
}
log("Checking for duplicate map IDs...");
HelpSet.parse(hsfile.toURI().toURL(), null, new VerifyHSFactory());
log("Checking links from help map and between HTML files...");
Enumeration e = map.getAllIDs();
Set<URI> okurls = new HashSet<URI>(1000);
Set<URI> badurls = new HashSet<URI>(1000);
Set<URI> cleanurls = new HashSet<URI>(1000);
while (e.hasMoreElements()) {
javax.help.Map.ID id = (javax.help.Map.ID)e.nextElement();
URL u = map.getURLFromID(id);
if (u == null) {
throw new BuildException("Bogus map ID: " + id.id, new Location(hsfile.getAbsolutePath()));
}
log("Checking ID " + id.id, Project.MSG_VERBOSE);
try {
CheckLinks.scan(this, null, null, id.id, "",
new URI(u.toExternalForm()), okurls, badurls, cleanurls, false, false, false, 2,
Collections.<Mapper>emptyList());
} catch (URISyntaxException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
private final class VerifyTIFactory implements TreeItemFactory {
private final HelpSet hs;
private final javax.help.Map map;
private final File navfile;
private final boolean toc;
public VerifyTIFactory(HelpSet hs, javax.help.Map map, File navfile, boolean toc) {
this.hs = hs;
this.map = map;
this.navfile = navfile;
this.toc = toc;
}
// The useful method:
public TreeItem createItem(String str, Hashtable hashtable, HelpSet helpSet, Locale locale) {
String target = (String)hashtable.get("target");
if (target != null) {
if (! map.isValidID(target, hs)) {
log(navfile + ": invalid map ID: " + target, Project.MSG_WARN);
} else {
log("OK map ID: " + target, Project.MSG_VERBOSE);
}
}
return createItem();
}
// Filler methods:
public java.util.Enumeration listMessages() {
return Collections.enumeration(Collections.<String>emptyList());
}
public void processPI(HelpSet helpSet, String str, String str2) {
}
public void reportMessage(String str, boolean param) {
log(str, param ? Project.MSG_VERBOSE : Project.MSG_WARN);
}
public void processDOCTYPE(String str, String str1, String str2) {
}
public void parsingStarted(URL uRL) {
}
public DefaultMutableTreeNode parsingEnded(DefaultMutableTreeNode defaultMutableTreeNode) {
return defaultMutableTreeNode;
}
public TreeItem createItem() {
if (toc) {
return new TOCItem();
} else {
return new IndexItem();
}
}
}
private final class VerifyHSFactory extends HelpSet.DefaultHelpSetFactory {
private Set<String> ids = new HashSet<String>(1000);
public void processMapRef(HelpSet hs, Hashtable attrs) {
try {
URL map = new URL(hs.getHelpSetURL(), (String)attrs.get("location"));
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setValidating(false);
factory.setNamespaceAware(false);
SAXParser parser = factory.newSAXParser();
parser.parse(new InputSource(map.toExternalForm()), new Handler(map.getFile()));
} catch (Exception e) {
e.printStackTrace();
}
}
private final class Handler extends DefaultHandler {
private final String map;
public Handler(String map) {
this.map = map;
}
public void startElement(String uri, String lname, String name, Attributes attributes) throws SAXException {
if (name.equals("mapID")) {
String target = attributes.getValue("target");
if (target != null) {
if (ids.add(target)) {
log("Found map ID: " + target, Project.MSG_DEBUG);
} else {
log(map + ": duplicated ID: " + target, Project.MSG_WARN);
}
}
}
}
public InputSource resolveEntity(String pub, String sys) throws SAXException {
if (pub.equals("-//Sun Microsystems Inc.//DTD JavaHelp Map Version 1.0//EN") ||
pub.equals("-//Sun Microsystems Inc.//DTD JavaHelp Map Version 2.0//EN")) {
// Ignore.
return new InputSource(new ByteArrayInputStream(new byte[0]));
} else {
return null;
}
}
}
}
*/
private static class NbDocsStreamHandler extends URLStreamHandler {
static class Factory implements URLStreamHandlerFactory {
public URLStreamHandler createURLStreamHandler(String protocol) {
if (protocol.equals("nbdocs")) { // NOI18N
return new CheckHelpSetsBin.NbDocsStreamHandler();
} else {
return null;
}
}
}
/** Make a URLConnection for nbdocs: URLs.
* @param u the URL
* @throws IOException if the wrong protocol
* @return the connection
*/
protected URLConnection openConnection(URL u) throws IOException {
if (u.getProtocol().equals("nbdocs")) { // NOI18N
return new NbDocsURLConnection(u);
} else {
throw new IOException();
}
}
/** A URL connection that reads from the docs classloader.
*/
static class NbDocsURLConnection extends URLConnection {
static ThreadLocal<URLClassLoader> globalClassLoader = new ThreadLocal<URLClassLoader>();
static ThreadLocal<Map<String,URLClassLoader>> classLoaderMap = new ThreadLocal<Map<String,URLClassLoader>>();
/** underlying URL connection
*/
private URLConnection real = null;
/** any associated exception while handling
*/
private IOException exception = null;
/** Make the connection.
* @param u URL to connect to
*/
public NbDocsURLConnection(URL u) {
super(u);
}
/** Connect to the URL.
* Actually look up and open the underlying connection.
* @throws IOException for the usual reasons
*/
public synchronized void connect() throws IOException {
tryToConnect();
if (exception != null) {
exception.printStackTrace();//XXX
throw exception;
}
}
/** Maybe connect, if not keep track of the problem.
*/
private synchronized void tryToConnect() {
if (connected || exception != null) {
return;
}
try {
URLClassLoader l;
String cnb = url.getHost();
if (cnb.isEmpty()) {
l = globalClassLoader.get();
} else {
l = classLoaderMap.get().get(cnb);
if (l == null) {
throw new IOException("no loader for " + cnb);
}
}
String path = url.getPath().substring(1);
URL u = l.getResource(path);
if (u == null) {
throw new FileNotFoundException(path + " in " + Arrays.toString(l.getURLs()));
}
real = u.openConnection();
real.connect();
connected = true;
} catch (IOException ioe) {
exception = ioe;
}
}
/** Get a URL header.
* @param n index of the header
* @return the header value
*/
public @Override String getHeaderField(int n) {
tryToConnect();
if (connected) {
return real.getHeaderField(n);
} else {
return null;
}
}
/** Get the name of a header.
* @param n the index
* @return the header name
*/
public @Override String getHeaderFieldKey(int n) {
tryToConnect();
if (connected) {
return real.getHeaderFieldKey(n);
} else {
return null;
}
}
/** Get a header by name.
* @param key the header name
* @return the value
*/
public @Override String getHeaderField(String key) {
tryToConnect();
if (connected) {
return real.getHeaderField(key);
} else {
return null;
}
}
/** Get an input stream on the connection.
* @throws IOException for the usual reasons
* @return a stream to the object
*/
public @Override InputStream getInputStream() throws IOException {
connect();
return real.getInputStream();
}
/** Get an output stream on the object.
* @throws IOException for the usual reasons
* @return an output stream writing to it
*/
public @Override OutputStream getOutputStream() throws IOException {
connect();
return real.getOutputStream();
}
/** Get the type of the content.
* @return the MIME type
*/
public @Override String getContentType() {
tryToConnect();
if (connected) {
return real.getContentType();
} else {
return "application/octet-stream";
}
}
/** Get the length of content.
* @return the length in bytes
*/
public @Override int getContentLength() {
tryToConnect();
if (connected) {
return real.getContentLength();
} else {
return 0;
}
}
}
}
}