001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.nbbuild;
043:
044: import java.io.*;
045: import java.lang.reflect.*;
046: import java.util.*;
047:
048: import org.apache.tools.ant.AntClassLoader;
049: import org.apache.tools.ant.BuildException;
050: import org.apache.tools.ant.DirectoryScanner;
051: import org.apache.tools.ant.FileScanner;
052: import org.apache.tools.ant.Project;
053: import org.apache.tools.ant.taskdefs.Copy;
054: import org.apache.tools.ant.taskdefs.Delete;
055: import org.apache.tools.ant.taskdefs.MatchingTask;
056: import org.apache.tools.ant.taskdefs.Mkdir;
057: import org.apache.tools.ant.types.FileSet;
058: import org.apache.tools.ant.types.Mapper;
059: import org.apache.tools.ant.types.Path;
060:
061: // ToDo:
062: // stopwords configuration
063: // verbose mode
064:
065: /** Task to run JavaHelp search indexer.
066: * Creates the proper binary search database from source HTML.
067: * @author Jesse Glick
068: * @see <a href="http://java.sun.com/products/javahelp/">JavaHelp home page</a>
069: */
070: public class JHIndexer extends MatchingTask {
071:
072: private Path classpath;
073: private File db;
074: private File basedir;
075: private String locale;
076: private List<BrandedFileSet> brandings = new LinkedList<BrandedFileSet>();
077:
078: /** Set the location of <samp>jhall.jar</samp> or <samp>jsearch.jar</samp> (JavaHelp tools library). */
079: public Path createClasspath() {
080: // JavaHelp release notes say jhtools.jar is enough, but class NoClassDefFoundError
081: // on javax.help.search.IndexBuilder when I tried it...
082: if (classpath == null) {
083: classpath = new Path(getProject());
084: }
085: return classpath.createPath();
086: }
087:
088: /** Set the location of the output database.
089: * E.g. <samp>JavaHelpSearch</samp>).
090: * <strong>Warning:</strong> the directory will be deleted and recreated.
091: */
092: public void setDb(File db) {
093: this .db = db;
094: }
095:
096: /** Set the base directory from which to scan files.
097: * This should be the directory containing the helpset for the database to work correctly.
098: */
099: public void setBasedir(File basedir) {
100: this .basedir = basedir;
101: }
102:
103: public void setLocale(String locale) {
104: this .locale = locale;
105: }
106:
107: /**
108: * A set of additional files forming a branding variant.
109: * @see #addBrandedFileSet
110: */
111: public static final class BrandedFileSet extends FileSet {
112: String branding;
113:
114: public void setBranding(String b) {
115: this .branding = b;
116: }
117: }
118:
119: /**
120: * Add a set of branded files to be indexed.
121: * For example, you may have in <samp>/the/base/dir</samp>
122: * <ul>
123: * <li><samp>foo.html</samp>
124: * <li><samp>bar.html</samp>
125: * <li><samp>baz.html</samp>
126: * </ul>
127: * Now create a new directory <samp>/the/new/dir</samp>:
128: * <ul>
129: * <li><samp>foo_brand.html</samp>
130: * <li><samp>baz_brand.html</samp>
131: * </ul>
132: * If you include this with:
133: * <pre>
134: <jhindexer basedir="/the/base/dir">
135: <include name="**/*.html"/>
136: <brandedfileset dir="/the/new/dir" branding="brand">
137: <include name="**/*.html"/>
138: </brandedfileset>
139: </jhindexer>
140: * </pre>
141: * then the search database will contain entries:
142: * <table border="1">
143: * <tr><th>JH name</th><th>From file</th></tr>
144: * <tr><td><samp>foo.html</samp></td><td><samp>/the/new/dir/foo_brand.html</samp></td></tr>
145: * <tr><td><samp>bar.html</samp></td><td><samp>/the/base/dir/bar.html</samp></td></tr>
146: * <tr><td><samp>baz.html</samp></td><td><samp>/the/new/dir/baz_brand.html</samp></td></tr>
147: * </table>
148: * and every file in the database (<samp>TMAP</samp> etc.)
149: * will receive the special suffix <samp>_brand</samp>.
150: * <p>You may give multiple branding filesets, so long as the branding
151: * tokens supplied are nested: i.e. for every pair of tokens among the supplied
152: * filesets, one is a prefix of the other (with <samp>_</samp> being the
153: * separator between the prefix and suffix). The search database suffix is then
154: * an underscore followed by the longest branding token.
155: * <p>Such a database is suitable for branding NetBeans: consider a module
156: * with documentation entries such as the following:
157: * <ul>
158: * <li><samp>modules/docs/foo.jar!/some/pkg/foo/foo.html</samp>
159: * <li><samp>modules/docs/foo.jar!/some/pkg/foo/bar.html</samp>
160: * <li><samp>modules/docs/foo.jar!/some/pkg/foo/baz.html</samp>
161: * <li><samp>modules/docs/foo.jar!/some/pkg/foo/JavaHelpSearch/TMAP</samp> (etc.)
162: * <li><samp>modules/docs/locale/foo_brand.jar!/some/pkg/foo/foo_brand.html</samp>
163: * <li><samp>modules/docs/locale/foo_brand.jar!/some/pkg/foo/baz_brand.html</samp>
164: * <li><samp>modules/docs/locale/foo_brand.jar!/some/pkg/foo/JavaHelpSearch/TMAP_brand</samp> (etc.)
165: * </ul>
166: * where the files in <samp>modules/docs/foo.jar!/some/pkg/foo/JavaHelpSearch/</samp>
167: * were generated by a regular invocation of this task and the files in
168: * <samp>modules/docs/locale/foo_brand.jar!/some/pkg/foo/JavaHelpSearch/</samp>
169: * were generated by the variant above. Then a help set reference using a URL such as
170: * <samp>nbdocs:/some/pkg/foo/helpset.xml</samp> will, when running with branding
171: * <samp>brand</samp>, not only display the expected variants of <samp>foo.html</samp>
172: * and <samp>baz.html</samp>, but be able to search for strings specifically in them
173: * (including correct offsets).
174: * @see <a href="http://www.netbeans.org/issues/show_bug.cgi?id=31044">Issue #31044</a>
175: */
176: public void addBrandedFileSet(BrandedFileSet s) {
177: brandings.add(s);
178: }
179:
180: /** @deprecated Use {@link #createClasspath} instead. */
181: @Deprecated
182: public void setJhall(File f) {
183: log(
184: "The 'jhall' attribute to <jhindexer> is deprecated. Use a nested <classpath> instead.",
185: Project.MSG_WARN);
186: createClasspath().setLocation(f);
187: }
188:
189: /**
190: *
191: */
192: public void setClassPath(Path cp) {
193: classpath = cp;
194: }
195:
196: public void execute() throws BuildException {
197: if (classpath == null)
198: throw new BuildException(
199: "Must specify the classpath attribute to find jhall.jar or jsearch.jar");
200: if (db == null)
201: throw new BuildException("Must specify the db attribute");
202: if (basedir == null)
203: throw new BuildException(
204: "Must specify the basedir attribute");
205: FileScanner scanner = getDirectoryScanner(basedir);
206: scanner.scan();
207: String[] files = scanner.getIncludedFiles();
208: // First, an up-to-date check. ;-)
209: if (basedir.exists() && db.exists()) {
210: long lastModified = Long.MIN_VALUE;
211: // First scan output dir for any files.
212: FileScanner output = new DirectoryScanner();
213: output.setBasedir(db);
214: output.scan();
215: String[] outfiles = output.getIncludedFiles();
216: if (outfiles.length > 0) {
217: for (int i = 0; i < outfiles.length; i++) {
218: long mod = new File(db, outfiles[i]).lastModified();
219: if (mod > lastModified) {
220: lastModified = mod;
221: }
222: }
223: // Now check to see if any source files are newer.
224: boolean ok = true;
225: for (int i = 0; i < files.length; i++) {
226: long mod = new File(basedir, files[i])
227: .lastModified();
228: if (mod > lastModified) {
229: ok = false;
230: break;
231: }
232: }
233: if (!brandings.isEmpty()) {
234: // Check these too!
235: for (FileSet fs : brandings) {
236: FileScanner scanner2 = fs
237: .getDirectoryScanner(getProject());
238: String[] files2 = scanner2.getIncludedFiles();
239: for (int i = 0; i < files2.length; i++) {
240: long mod = new File(basedir, files2[i])
241: .lastModified();
242: if (mod > lastModified) {
243: ok = false;
244: break;
245: }
246: }
247: }
248: }
249: if (ok) {
250: // No need to rebuild.
251: return;
252: }
253: }
254: }
255: Delete delete = (Delete) getProject().createTask("delete");
256: delete.setDir(db);
257: delete.init();
258: delete.setLocation(getLocation());
259: delete.execute();
260: Mkdir mkdir = (Mkdir) getProject().createTask("mkdir");
261: mkdir.setDir(db);
262: mkdir.init();
263: mkdir.setLocation(getLocation());
264: mkdir.execute();
265: String maxbranding = null;
266: if (!brandings.isEmpty()) {
267: // Copy all files, overriding by branding, to a fresh dir somewhere.
268: // Does not suffice to simply use IndexRemove to strip off the basedirs
269: // of files in branded filesets, since their filenames will also include
270: // the branding token, and this will mess up the search database: it needs
271: // to store just the simple file name with no branding infix.
272: File tmp = new File(System.getProperty("java.io.tmpdir"),
273: "jhindexer-branding-merge");
274: delete = (Delete) getProject().createTask("delete");
275: delete.setDir(tmp);
276: delete.init();
277: delete.setLocation(getLocation());
278: delete.execute();
279: tmp.mkdir();
280: // Start with the base files.
281: Copy copy = (Copy) getProject().createTask("copy");
282: copy.setTodir(tmp);
283: copy.addFileset(fileset);
284: copy.init();
285: copy.setLocation(getLocation());
286: copy.execute();
287: // Now branded filesets. Must be done in order of branding, so that
288: // more specific files override generic ones.
289: class BrandingLengthComparator implements
290: Comparator<BrandedFileSet> {
291: public int compare(BrandedFileSet a, BrandedFileSet b) {
292: return a.branding.length() - b.branding.length();
293: }
294: }
295: Collections.sort(brandings, new BrandingLengthComparator());
296: for (BrandedFileSet s : brandings) {
297: if (maxbranding != null
298: && !s.branding.startsWith(maxbranding + "_")) {
299: throw new BuildException("Illegal branding: "
300: + s.branding, getLocation());
301: }
302: maxbranding = s.branding; // only last one will be kept
303: String[] suffixes = { ".html", ".htm", ".xhtml",
304: // XXX any others? unpleasant to hardcode but this is easiest,
305: // since glob mappers do not permit *_x* -> ** syntax.
306: };
307: for (int i = 0; i < suffixes.length; i++) {
308: String suffix = suffixes[i];
309: copy = (Copy) getProject().createTask("copy");
310: copy.setTodir(tmp);
311: copy.setOverwrite(true);
312: copy.addFileset(s);
313: Mapper m = copy.createMapper();
314: Mapper.MapperType mt = new Mapper.MapperType();
315: mt.setValue("glob");
316: m.setType(mt);
317: m.setFrom("*_" + s.branding + suffix);
318: m.setTo("*" + suffix);
319: copy.init();
320: copy.setLocation(getLocation());
321: copy.execute();
322: if (locale != null) {
323: // Possibly have e.g. x_f4j_ja.html.
324: suffix = "_" + locale + suffix;
325: copy = (Copy) getProject().createTask("copy");
326: copy.setTodir(tmp);
327: copy.setOverwrite(true);
328: copy.addFileset(s);
329: m = copy.createMapper();
330: mt = new Mapper.MapperType();
331: mt.setValue("glob");
332: m.setType(mt);
333: m.setFrom("*_" + s.branding + suffix);
334: m.setTo("*" + suffix);
335: copy.init();
336: copy.setLocation(getLocation());
337: copy.execute();
338: }
339: }
340: }
341: // Now replace basedir & files with this temp dir.
342: basedir = tmp;
343: FileSet tmpf = new FileSet();
344: tmpf.setProject(getProject());
345: tmpf.setDir(tmp);
346: files = tmpf.getDirectoryScanner(getProject())
347: .getIncludedFiles();
348: }
349: log("Running JavaHelp search database indexer...");
350: try {
351: File config = File.createTempFile("jhindexer-config",
352: ".txt");
353: try {
354: OutputStream os = new FileOutputStream(config);
355: try {
356: PrintWriter pw = new PrintWriter(os);
357: pw.println("IndexRemove " + basedir
358: + File.separator);
359: String message = "Files to be indexed:";
360: for (int i = 0; i < files.length; i++) {
361: // [PENDING] JavaHelp docs say to use / as file sep for File directives;
362: // so what should the complete path be? Someone should test this on Windoze...
363: String path = basedir + File.separator
364: + files[i];
365: pw.println("File " + path);
366: message += "\n\t" + path;
367: }
368: log(message, Project.MSG_VERBOSE);
369: pw.flush();
370: } finally {
371: os.close();
372: }
373: AntClassLoader loader = new AntClassLoader(
374: getProject(), classpath);
375: try {
376: Class<?> clazz = loader
377: .loadClass("com.sun.java.help.search.Indexer");
378: Method main = clazz.getMethod("main",
379: String[].class);
380: List<String> args = Arrays.asList("-c", config
381: .getAbsolutePath(), "-db", db
382: .getAbsolutePath());
383: if (locale != null) {
384: args = new ArrayList<String>(args); // #35244
385: args.add("-locale");
386: args.add(locale);
387: }
388: main.invoke(null, new Object[] { args
389: .toArray(new String[args.size()]) });
390: } catch (InvocationTargetException ite) {
391: throw new BuildException("Could not run indexer",
392: ite.getTargetException(), getLocation());
393: } catch (Exception e) { // ClassNotFoundException, NoSuchMethodException, ...
394: throw new BuildException("Could not run indexer",
395: e, getLocation());
396: }
397: } finally {
398: config.delete();
399: }
400: } catch (IOException ioe) {
401: throw new BuildException(
402: "Could not make temporary config file", ioe,
403: getLocation());
404: }
405: if (maxbranding != null) {
406: // Now rename search DB files to include branding suffix.
407: // Note that DOCS.TAB -> DOCS_brand.TAB to work with nbdocs: protocol.
408: String[] dbfiles = db.list();
409: for (int i = 0; i < dbfiles.length; i++) {
410: String basename, ext;
411: int idx = dbfiles[i].lastIndexOf('.');
412: if (idx != -1) {
413: basename = dbfiles[i].substring(0, idx);
414: ext = dbfiles[i].substring(idx);
415: } else {
416: basename = dbfiles[i];
417: ext = "";
418: }
419: File old = new File(db, dbfiles[i]);
420: File nue = new File(db, basename + "_" + maxbranding
421: + ext);
422: log("Moving " + old + " to " + nue, Project.MSG_VERBOSE);
423: old.renameTo(nue);
424: }
425: }
426: }
427:
428: }
|