blob: 745b630d1470e9b6e1af3b7445d8ccaf84a4c718 [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.
*/
package org.netbeans.nbbuild;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import org.apache.tools.ant.AntClassLoader;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.FileScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.taskdefs.Delete;
import org.apache.tools.ant.taskdefs.MatchingTask;
import org.apache.tools.ant.taskdefs.Mkdir;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Mapper;
import org.apache.tools.ant.types.Path;
// ToDo:
// stopwords configuration
// verbose mode
/** Task to run JavaHelp search indexer.
* Creates the proper binary search database from source HTML.
* @author Jesse Glick
* @see <a href="http://java.sun.com/products/javahelp/">JavaHelp home page</a>
*/
public class JHIndexer extends MatchingTask {
private Path classpath;
private File db;
private File basedir;
private String locale;
private List<BrandedFileSet> brandings = new LinkedList<>();
/** Set the location of <samp>jhall.jar</samp> or <samp>jsearch.jar</samp> (JavaHelp tools library). */
public Path createClasspath() {
// JavaHelp release notes say jhtools.jar is enough, but class NoClassDefFoundError
// on javax.help.search.IndexBuilder when I tried it...
if (classpath == null) {
classpath = new Path(getProject());
}
return classpath.createPath();
}
/** Set the location of the output database.
* E.g. <samp>JavaHelpSearch</samp>).
* <strong>Warning:</strong> the directory will be deleted and recreated.
*/
public void setDb (File db) {
this.db = db;
}
/** Set the base directory from which to scan files.
* This should be the directory containing the helpset for the database to work correctly.
*/
public void setBasedir (File basedir) {
this.basedir = basedir;
}
public void setLocale (String locale) {
this.locale = locale;
}
/**
* A set of additional files forming a branding variant.
* @see #addBrandedFileSet
*/
public static final class BrandedFileSet extends FileSet {
String branding;
public void setBranding(String b) {
this.branding = b;
}
}
/**
* Add a set of branded files to be indexed.
* For example, you may have in <samp>/the/base/dir</samp>
* <ul>
* <li><samp>foo.html</samp>
* <li><samp>bar.html</samp>
* <li><samp>baz.html</samp>
* </ul>
* Now create a new directory <samp>/the/new/dir</samp>:
* <ul>
* <li><samp>foo_brand.html</samp>
* <li><samp>baz_brand.html</samp>
* </ul>
* If you include this with:
* <pre>
&lt;jhindexer basedir="/the/base/dir"&gt;
&lt;include name="*&#42;/*.html"/&gt;
&lt;brandedfileset dir="/the/new/dir" branding="brand"&gt;
&lt;include name="*&#42;/*.html"/&gt;
&lt;/brandedfileset&gt;
&lt;/jhindexer&gt;
* </pre>
* then the search database will contain entries:
* <table border="1">
* <tr><th>JH name</th><th>From file</th></tr>
* <tr><td><samp>foo.html</samp></td><td><samp>/the/new/dir/foo_brand.html</samp></td></tr>
* <tr><td><samp>bar.html</samp></td><td><samp>/the/base/dir/bar.html</samp></td></tr>
* <tr><td><samp>baz.html</samp></td><td><samp>/the/new/dir/baz_brand.html</samp></td></tr>
* </table>
* and every file in the database (<samp>TMAP</samp> etc.)
* will receive the special suffix <samp>_brand</samp>.
* <p>You may give multiple branding filesets, so long as the branding
* tokens supplied are nested: i.e. for every pair of tokens among the supplied
* filesets, one is a prefix of the other (with <samp>_</samp> being the
* separator between the prefix and suffix). The search database suffix is then
* an underscore followed by the longest branding token.
* <p>Such a database is suitable for branding NetBeans: consider a module
* with documentation entries such as the following:
* <ul>
* <li><samp>modules/docs/foo.jar!/some/pkg/foo/foo.html</samp>
* <li><samp>modules/docs/foo.jar!/some/pkg/foo/bar.html</samp>
* <li><samp>modules/docs/foo.jar!/some/pkg/foo/baz.html</samp>
* <li><samp>modules/docs/foo.jar!/some/pkg/foo/JavaHelpSearch/TMAP</samp> (etc.)
* <li><samp>modules/docs/locale/foo_brand.jar!/some/pkg/foo/foo_brand.html</samp>
* <li><samp>modules/docs/locale/foo_brand.jar!/some/pkg/foo/baz_brand.html</samp>
* <li><samp>modules/docs/locale/foo_brand.jar!/some/pkg/foo/JavaHelpSearch/TMAP_brand</samp> (etc.)
* </ul>
* where the files in <samp>modules/docs/foo.jar!/some/pkg/foo/JavaHelpSearch/</samp>
* were generated by a regular invocation of this task and the files in
* <samp>modules/docs/locale/foo_brand.jar!/some/pkg/foo/JavaHelpSearch/</samp>
* were generated by the variant above. Then a help set reference using a URL such as
* <samp>nbdocs:/some/pkg/foo/helpset.xml</samp> will, when running with branding
* <samp>brand</samp>, not only display the expected variants of <samp>foo.html</samp>
* and <samp>baz.html</samp>, but be able to search for strings specifically in them
* (including correct offsets).
* @see <a href="http://www.netbeans.org/issues/show_bug.cgi?id=31044">Issue #31044</a>
*/
public void addBrandedFileSet(BrandedFileSet s) {
brandings.add(s);
}
/** @deprecated Use {@link #createClasspath} instead. */
@Deprecated
public void setJhall(File f) {
log("The 'jhall' attribute to <jhindexer> is deprecated. Use a nested <classpath> instead.", Project.MSG_WARN);
createClasspath().setLocation(f);
}
/**
*
*/
public void setClassPath(Path cp) {
classpath = cp;
}
public @Override void execute() throws BuildException {
if (classpath == null) {
throw new BuildException ("Must specify the classpath attribute to find jhall.jar or jsearch.jar");
}
if (db == null) {
throw new BuildException ("Must specify the db attribute");
}
if (basedir == null) {
throw new BuildException ("Must specify the basedir attribute");
}
FileScanner scanner = getDirectoryScanner (basedir);
scanner.scan ();
String[] files = scanner.getIncludedFiles ();
// First, an up-to-date check. ;-)
if (basedir.exists () && db.exists ()) {
long lastModified = Long.MIN_VALUE;
// First scan output dir for any files.
FileScanner output = new DirectoryScanner ();
output.setBasedir (db);
output.scan ();
String[] outfiles = output.getIncludedFiles ();
if (outfiles.length > 0) {
for (int i = 0; i < outfiles.length; i++) {
long mod = new File (db, outfiles[i]).lastModified ();
if (mod > lastModified) {
lastModified = mod;
}
}
// Now check to see if any source files are newer.
boolean ok = true;
for (int i = 0; i < files.length; i++) {
long mod = new File (basedir, files[i]).lastModified ();
if (mod > lastModified) {
ok = false;
break;
}
}
if (!brandings.isEmpty()) {
// Check these too!
for(FileSet fs: brandings) {
FileScanner scanner2 = fs.getDirectoryScanner(getProject());
String[] files2 = scanner2.getIncludedFiles();
for (int i = 0; i < files2.length; i++) {
long mod = new File (basedir, files2[i]).lastModified ();
if (mod > lastModified) {
ok = false;
break;
}
}
}
}
if (ok) {
// No need to rebuild.
return;
}
}
}
Delete delete = (Delete) getProject().createTask("delete");
delete.setDir (db);
delete.init ();
delete.setLocation(getLocation());
delete.execute ();
Mkdir mkdir = (Mkdir) getProject().createTask("mkdir");
mkdir.setDir (db);
mkdir.init ();
mkdir.setLocation(getLocation());
mkdir.execute ();
String maxbranding = null;
if (!brandings.isEmpty()) {
// Copy all files, overriding by branding, to a fresh dir somewhere.
// Does not suffice to simply use IndexRemove to strip off the basedirs
// of files in branded filesets, since their filenames will also include
// the branding token, and this will mess up the search database: it needs
// to store just the simple file name with no branding infix.
File tmp = new File(System.getProperty("java.io.tmpdir"), "jhindexer-branding-merge");
delete = (Delete) getProject().createTask("delete");
delete.setDir(tmp);
delete.init();
delete.setLocation(getLocation());
delete.execute();
tmp.mkdir();
// Start with the base files.
Copy copy = (Copy) getProject().createTask("copy");
copy.setTodir(tmp);
copy.addFileset(fileset);
copy.init();
copy.setLocation(getLocation());
copy.execute();
// Now branded filesets. Must be done in order of branding, so that
// more specific files override generic ones.
class BrandingLengthComparator implements Comparator<BrandedFileSet> {
public int compare(BrandedFileSet a, BrandedFileSet b) {
return a.branding.length() - b.branding.length();
}
}
Collections.sort(brandings, new BrandingLengthComparator());
for (BrandedFileSet s: brandings) {
if (maxbranding != null && !s.branding.startsWith(maxbranding + "_")) {
throw new BuildException("Illegal branding: " + s.branding, getLocation());
}
maxbranding = s.branding; // only last one will be kept
String[] suffixes = {
".html",
".htm",
".xhtml",
// XXX any others? unpleasant to hardcode but this is easiest,
// since glob mappers do not permit *_x* -> ** syntax.
};
for (int i = 0; i < suffixes.length; i++) {
String suffix = suffixes[i];
copy = (Copy) getProject().createTask("copy");
copy.setTodir(tmp);
copy.setOverwrite(true);
copy.addFileset(s);
Mapper m = copy.createMapper();
Mapper.MapperType mt = new Mapper.MapperType();
mt.setValue("glob");
m.setType(mt);
m.setFrom("*_" + s.branding + suffix);
m.setTo("*" + suffix);
copy.init();
copy.setLocation(getLocation());
copy.execute();
if (locale != null) {
// Possibly have e.g. x_f4j_ja.html.
suffix = "_" + locale + suffix;
copy = (Copy) getProject().createTask("copy");
copy.setTodir(tmp);
copy.setOverwrite(true);
copy.addFileset(s);
m = copy.createMapper();
mt = new Mapper.MapperType();
mt.setValue("glob");
m.setType(mt);
m.setFrom("*_" + s.branding + suffix);
m.setTo("*" + suffix);
copy.init();
copy.setLocation(getLocation());
copy.execute();
}
}
}
// Now replace basedir & files with this temp dir.
basedir = tmp;
FileSet tmpf = new FileSet();
tmpf.setProject(getProject());
tmpf.setDir(tmp);
files = tmpf.getDirectoryScanner(getProject()).getIncludedFiles();
}
log ("Running JavaHelp search database indexer...");
try {
File config = File.createTempFile ("jhindexer-config", ".txt");
try {
try (OutputStream os = new FileOutputStream (config)) {
PrintWriter pw = new PrintWriter (os);
pw.println ("IndexRemove " + basedir + File.separator);
String message = "Files to be indexed:";
for (int i = 0; i < files.length; i++) {
// [PENDING] JavaHelp docs say to use / as file sep for File directives;
// so what should the complete path be? Someone should test this on Windoze...
String path = basedir + File.separator + files[i];
pw.println ("File " + path);
message += "\n\t" + path;
}
log (message, Project.MSG_VERBOSE);
pw.flush ();
}
AntClassLoader loader = new AntClassLoader(getProject(), classpath);
loader.addLoaderPackageRoot("com.sun.java.help.search");
try {
Class<?> clazz = loader.loadClass("com.sun.java.help.search.Indexer");
Method main = clazz.getMethod("main", String[].class);
List<String> args = Arrays.asList(
"-c", config.getAbsolutePath(),
"-db", db.getAbsolutePath()
);
if (locale != null) {
args = new ArrayList<>(args); // #35244
args.add("-locale");
args.add(locale);
}
main.invoke(null, new Object[] {args.toArray(new String[args.size()])});
} catch (InvocationTargetException ite) {
throw new BuildException(ite.getTargetException(), getLocation());
} catch (Exception e) { // ClassNotFoundException, NoSuchMethodException, ...
throw new BuildException(e, getLocation());
}
} finally {
config.delete ();
}
} catch (IOException ioe) {
throw new BuildException("Could not make temporary config file", ioe, getLocation());
}
if (maxbranding != null) {
// Now rename search DB files to include branding suffix.
// Note that DOCS.TAB -> DOCS_brand.TAB to work with nbdocs: protocol.
String[] dbfiles = db.list();
for (int i = 0; i < dbfiles.length; i++) {
String basename, ext;
int idx = dbfiles[i].lastIndexOf('.');
if (idx != -1) {
basename = dbfiles[i].substring(0, idx);
ext = dbfiles[i].substring(idx);
} else {
basename = dbfiles[i];
ext = "";
}
File old = new File(db, dbfiles[i]);
File nue = new File(db, basename + "_" + maxbranding + ext);
log("Moving " + old + " to " + nue, Project.MSG_VERBOSE);
old.renameTo(nue);
}
}
}
}