SampleCode.java revision e9ab1b94fc0414fe72646b90b355562abf98d86e
1/*
2 * Copyright (C) 2010 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.google.doclava;
18
19import java.util.Arrays;
20import java.util.ArrayList;
21import java.util.Collections;
22import java.util.Comparator;
23import java.util.List;
24import java.util.regex.Pattern;
25import java.util.regex.Matcher;
26import java.io.File;
27
28import com.google.clearsilver.jsilver.data.Data;
29
30/**
31* Represents a browsable sample code project, with methods for managing
32* metadata collection, file output, sorting, etc.
33*/
34public class SampleCode {
35  String mSource;
36  String mDest;
37  String mTitle;
38  String mProjectDir;
39  String mTags;
40
41  /** Max size for browseable images/video. If a source file exceeds this size,
42  * a file is generated with a generic placeholder and the original file is not
43  * copied to out.
44  */
45  private static final double MAX_FILE_SIZE_BYTES = 2097152;
46
47  /** When full tree nav is enabled, generate an index for every dir
48  * and linkify the breadcrumb paths in all files.
49  */
50  private static final boolean FULL_TREE_NAVIGATION = false;
51
52  public SampleCode(String source, String dest, String title) {
53    mSource = source;
54    mTitle = title;
55    mTags = null;
56
57    if (dest != null) {
58      int len = dest.length();
59      if (len > 1 && dest.charAt(len - 1) != '/') {
60        mDest = dest + '/';
61      } else {
62        mDest = dest;
63      }
64    }
65  }
66
67  /**
68  * Iterates a given sample code project gathering  metadata for files and building
69  * a node tree that reflects the project's directory structure. After iterating
70  * the project, this method adds the project's metadata to jd_lists_unified,
71  * so that it is accessible for dynamic content and search suggestions.
72  *
73  * @param offlineMode Ignored -- offline-docs mode is not currently supported for
74  *        browsable sample code projects.
75  * @return A root Node for the project containing its metadata and tree structure.
76  */
77  public Node setSamplesTOC(boolean offlineMode) {
78    List<Node> filelist = new ArrayList<Node>();
79    File f = new File(mSource);
80    mProjectDir = f.getName();
81    String name = mProjectDir;
82    String mOut = mDest + name;
83    if (!f.isDirectory()) {
84      System.out.println("-samplecode not a directory: " + mSource);
85      return null;
86    }
87
88    Data hdf = Doclava.makeHDF();
89    setProjectStructure(filelist, f, mDest);
90    String link = ClearPage.toroot + "samples/" + name + "/index" + Doclava.htmlExtension;
91    Node rootNode = writeSampleIndexCs(hdf, f,
92        new Node.Builder().setLabel(mProjectDir).setLink(link).setChildren(filelist).build(),false);
93    return rootNode;
94  }
95
96  /**
97  * For a given sample code project dir, iterate through the project generating
98  * browsable html for all valid sample code files. After iterating the project
99  * generate a templated index file to the project output root.
100  *
101  * @param offlineMode Ignored -- offline-docs mode is not currently supported for
102  *        browsable sample code projects.
103  */
104  public void writeSamplesFiles(boolean offlineMode) {
105    List<Node> filelist = new ArrayList<Node>();
106    File f = new File(mSource);
107    mProjectDir = f.getName();
108    String name = mProjectDir;
109    String mOut = mDest + name;
110    if (!f.isDirectory()) {
111      System.out.println("-samplecode not a directory: " + mSource);
112    }
113
114    Data hdf = Doclava.makeHDF();
115    if (Doclava.samplesNavTree != null) {
116      hdf.setValue("samples_toc_tree", Doclava.samplesNavTree.getValue("samples_toc_tree", ""));
117    }
118    hdf.setValue("samples", "true");
119    hdf.setValue("projectDir", mProjectDir);
120    writeProjectDirectory(f, mDest, false, hdf, "Files.");
121    writeProjectStructure(name, hdf);
122    hdf.removeTree("parentdirs");
123    hdf.setValue("parentdirs.0.Name", name);
124    boolean writeFiles = true;
125    String link = "samples/" + name + "/index" + Doclava.htmlExtension;
126    //Write root _index.jd to out and add metadata to Node.
127    writeSampleIndexCs(hdf, f,
128        new Node.Builder().setLabel(mProjectDir).setLink(link).build(), true);
129  }
130
131  /**
132  * Given the root Node for a sample code project, iterates through the project
133  * gathering metadata and project tree structure. Unsupported file types are
134  * filtered from the project output. The collected project Nodes are appended to
135  * the root project node.
136  *
137  * @param parent The root Node that represents this sample code project.
138  * @param dir The current dir being processed.
139  * @param relative Relative path for creating links to this file.
140  */
141  public void setProjectStructure(List<Node> parent, File dir, String relative) {
142    String name, link;
143    File[] dirContents = dir.listFiles();
144    Arrays.sort(dirContents, byTypeAndName);
145    for (File f: dirContents) {
146      name = f.getName();
147      if (!isValidFiletype(name)) {
148        continue;
149      }
150      if (f.isFile() && name.contains(".")) {
151        String path = relative + name;
152        link = convertExtension(path, Doclava.htmlExtension);
153        if (inList(path, IMAGES) || inList(path, VIDEOS) || inList(path, TEMPLATED)) {
154          parent.add(new Node.Builder().setLabel(name).setLink(ClearPage.toroot + link).build());
155        }
156      } else if (f.isDirectory()) {
157        List<Node> mchildren = new ArrayList<Node>();
158        String dirpath = relative + name + "/";
159        setProjectStructure(mchildren, f, dirpath);
160        if (mchildren.size() > 0) {
161          parent.add(new Node.Builder().setLabel(name).setLink(ClearPage.toroot
162            + dirpath).setChildren(mchildren).build());
163        }
164      }
165    }
166  }
167
168  /**
169  * Given a root sample code project path, iterates through the project
170  * setting page metadata to manage html output and writing/copying files to
171  * the output directory. Source files are templated and images are templated
172  * and linked to the original image.
173  *
174  * @param dir The current dir being processed.
175  * @param relative Relative path for creating links to this file.
176  * @param recursed Whether the method is being called recursively.
177  * @param hdf The data to read/write for files in this project.
178  * @param newKey Key passed in recursion for managing cs child trees.
179  */
180  public void writeProjectDirectory(File dir, String relative, Boolean recursed,
181      Data hdf, String newkey) {
182    String name = "";
183    String link = "";
184    String type = "";
185    int i = 0;
186    String expansion = ".Sub.";
187    String key = newkey;
188
189    if (recursed) {
190      key = (key + expansion);
191    } else {
192      expansion = "";
193    }
194
195    File[] dirContents = dir.listFiles();
196    Arrays.sort(dirContents, byTypeAndName);
197    for (File f: dirContents) {
198      name = f.getName();
199      if (!isValidFiletype(name)) {
200        continue;
201      }
202      if (f.isFile() && name.contains(".")) {
203        String path = relative + name;
204        type = mapTypes(name);
205        link = convertExtension(path, Doclava.htmlExtension);
206        if (inList(path, IMAGES)) {
207          type = "img";
208          if (f.length() < MAX_FILE_SIZE_BYTES) {
209            ClearPage.copyFile(false, f, path);
210            writeImageVideoPage(f, convertExtension(path, Doclava.htmlExtension),
211                relative, type, true);
212          } else {
213            writeImageVideoPage(f, convertExtension(path, Doclava.htmlExtension),
214                relative, type, false);
215          }
216          hdf.setValue(key + i + ".Type", "img");
217          hdf.setValue(key + i + ".Name", name);
218          hdf.setValue(key + i + ".Href", link);
219          hdf.setValue(key + i + ".RelPath", relative);
220        } else if (inList(path, VIDEOS)) {
221          type = "video";
222          if (f.length() < MAX_FILE_SIZE_BYTES) {
223            ClearPage.copyFile(false, f, path);
224            writeImageVideoPage(f, convertExtension(path, Doclava.htmlExtension),
225                relative, type, true);
226          } else {
227            writeImageVideoPage(f, convertExtension(path, Doclava.htmlExtension),
228                relative, type, false);
229          }
230          hdf.setValue(key + i + ".Type", "video");
231          hdf.setValue(key + i + ".Name", name);
232          hdf.setValue(key + i + ".Href", link);
233          hdf.setValue(key + i + ".RelPath", relative);
234        } else if (inList(path, TEMPLATED)) {
235          writePage(f, convertExtension(path, Doclava.htmlExtension), relative, hdf);
236          hdf.setValue(key + i + ".Type", type);
237          hdf.setValue(key + i + ".Name", name);
238          hdf.setValue(key + i + ".Href", link);
239          hdf.setValue(key + i + ".RelPath", relative);
240        }
241        i++;
242      } else if (f.isDirectory()) {
243        List<Node> mchildren = new ArrayList<Node>();
244        type = "dir";
245        String dirpath = relative + name;
246        link = dirpath + "/index" + Doclava.htmlExtension;
247         String hdfkeyName = (key + i + ".Name");
248         String hdfkeyType = (key + i + ".Type");
249         String hdfkeyHref = (key + i + ".Href");
250        hdf.setValue(hdfkeyName, name);
251        hdf.setValue(hdfkeyType, type);
252        hdf.setValue(hdfkeyHref, relative + name + "/" + "index" + Doclava.htmlExtension);
253        writeProjectDirectory(f, relative + name + "/", true, hdf, (key + i));
254        i++;
255      }
256    }
257
258    setParentDirs(hdf, relative, name, false);
259    //Generate an index.html page for each dir being processed
260    if (FULL_TREE_NAVIGATION) {
261      ClearPage.write(hdf, "sampleindex.cs", relative + "/index" + Doclava.htmlExtension);
262    }
263  }
264
265  /**
266  * Processes a templated project index page from _index.jd in a project root.
267  * Each sample project must have an index, and each index locally defines it's own
268  * page.tags and sample.group cs vars. This method takes a SC node on input, reads
269  * any local vars from the _index.jd, optionally generates an html file to out,
270  * then updates the SC node with the page vars and returns it to the caller.
271  *
272  * @param hdf The data source to read/write for this index file.
273  * @param dir The sample project root directory.
274  * @param tnode A Node to serve as the project's root node.
275  * @param writeFiles If true, generates output files only. If false, collects
276  *        metadata only.
277  * @return The tnode root with any metadata/child Nodes appended.
278  */
279  public Node writeSampleIndexCs(Data hdf, File dir, Node tnode, boolean writeFiles) {
280
281    String filename = dir.getAbsolutePath() + "/_index.jd";
282    String mGroup = "";
283    File f = new File(filename);
284    String rel = dir.getPath();
285    if (writeFiles) {
286
287      hdf.setValue("samples", "true");
288      //set any default page variables for root index
289      hdf.setValue("page.title", mProjectDir);
290      hdf.setValue("projectDir", mProjectDir);
291      hdf.setValue("projectTitle", mTitle);
292      //add the download/project links to the landing pages.
293      hdf.setValue("samplesProjectIndex", "true");
294      if (!f.isFile()) {
295        //The directory didn't have an _index.jd, so create a stub.
296        ClearPage.write(hdf, "sampleindex.cs", mDest + "index" + Doclava.htmlExtension);
297      } else {
298        DocFile.writePage(filename, rel, mDest + "index" + Doclava.htmlExtension, hdf);
299        PageMetadata.setPageMetadata(f, rel, mDest + "index" + Doclava.htmlExtension,
300            hdf, Doclava.sTaglist);
301      }
302    } else if (f.isFile()) {
303      //gather metadata for toc and jd_lists_unified
304      DocFile.getPageMetadata(filename, hdf);
305      mGroup = hdf.getValue("sample.group", "");
306      if (!"".equals(mGroup)) {
307        tnode.setGroup(hdf.getValue("sample.group", ""));
308      } else {
309        //Errors.error(Errors.INVALID_SAMPLE_INDEX, null, "Sample " + mProjectDir
310        //          + ": Root _index.jd must be present and must define sample.group"
311        //          + " tag. Please see ... for details.");
312      }
313    }
314    return tnode;
315  }
316
317  /**
318  * Sets metadata for managing html output and generates the project view page
319  * for a project.
320  *
321  * @param dir The project root dir.
322  * @param hdf The data to read/write for files in this project.
323  */
324  public void writeProjectStructure(String dir, Data hdf) {
325    hdf.setValue("projectStructure", "true");
326    hdf.setValue("projectDir", mProjectDir);
327    hdf.setValue("page.title", mProjectDir + " Structure");
328    hdf.setValue("projectTitle", mTitle);
329    ClearPage.write(hdf, "sampleindex.cs", mDest + "project" + Doclava.htmlExtension);
330    hdf.setValue("projectStructure", "");
331  }
332
333  /**
334  * Keeps track of each file's parent dirs. Used for generating path breadcrumbs in html.
335  *
336  * @param dir The data to read/write for this file.
337  * @param hdf The relative path for this file, from samples root.
338  * @param subdir The relative path for this file, from samples root.
339  * @param name The name of the file (minus extension).
340  * @param isFile Whether this is a file (not a dir).
341  */
342  Data setParentDirs(Data hdf, String subdir, String name, Boolean isFile) {
343    if (FULL_TREE_NAVIGATION) {
344      hdf.setValue("linkfyPathCrumb", "");
345    }
346    int iter;
347    hdf.removeTree("parentdirs");
348    String s = subdir;
349    String urlParts[] = s.split("/");
350    int n, l = 1;
351    for (iter=1; iter < urlParts.length; iter++) {
352      n = iter-1;
353      hdf.setValue("parentdirs." + n + ".Name", urlParts[iter]);
354      hdf.setValue("parentdirs." + n + ".Link", subdir + "index" + Doclava.htmlExtension);
355    }
356    return hdf;
357  }
358
359  /**
360  * Writes a templated source code file to out.
361  */
362  public void writePage(File f, String out, String subdir, Data hdf) {
363    String name = f.getName();
364    String path = f.getPath();
365    String data = SampleTagInfo.readFile(new SourcePositionInfo(path, -1, -1), path,
366        "sample code", true, true, true, true);
367    data = Doclava.escape(data);
368
369    String relative = subdir.replaceFirst("samples/", "");
370    setParentDirs(hdf, subdir, name, true);
371    hdf.setValue("projectTitle", mTitle);
372    hdf.setValue("projectDir", mProjectDir);
373    hdf.setValue("page.title", name);
374    hdf.setValue("subdir", subdir);
375    hdf.setValue("relative", relative);
376    hdf.setValue("realFile", name);
377    hdf.setValue("fileContents", data);
378    hdf.setValue("resTag", "sample");
379
380    ClearPage.write(hdf, "sample.cs", out);
381  }
382
383  /**
384  * Writes a templated image or video file to out.
385  */
386  public void writeImageVideoPage(File f, String out, String subdir,
387        String resourceType, boolean browsable) {
388    Data hdf = Doclava.makeHDF();
389    if (Doclava.samplesNavTree != null) {
390      hdf.setValue("samples_toc_tree", Doclava.samplesNavTree.getValue("samples_toc_tree", ""));
391    }
392    hdf.setValue("samples", "true");
393
394    String name = f.getName();
395    if (!browsable) {
396      hdf.setValue("noDisplay", "true");
397    }
398    setParentDirs(hdf, subdir, name, true);
399    hdf.setValue("samples", "true");
400    hdf.setValue("page.title", name);
401    hdf.setValue("projectTitle", mTitle);
402    hdf.setValue("projectDir", mProjectDir);
403    hdf.setValue("subdir", subdir);
404    hdf.setValue("resType", resourceType);
405    hdf.setValue("realFile", name);
406
407    ClearPage.write(hdf, "sample.cs", out);
408  }
409
410  /**
411  * Given a node containing sample code projects and a node containing all valid
412  * group nodes, extract project nodes from tnode and append them to the group node
413  * that matches their sample.group metadata.
414  *
415  * @param tnode A list of nodes containing sample code projects.
416  * @param groupnodes A list of nodes that represent the valid sample groups.
417  * @return The groupnodes list with all projects appended properly to their
418  *         associated sample groups.
419  */
420  public static void writeSamplesNavTree(List<Node> tnode, List<Node> groupnodes) {
421
422    Node node = new Node.Builder().setLabel("Samples").setLink(ClearPage.toroot
423        + "samples/index" + Doclava.htmlExtension).setChildren(tnode).build();
424
425    if (groupnodes != null) {
426      for (int i = 0; i < tnode.size(); i++) {
427        if (tnode.get(i) != null) {
428          groupnodes = appendNodeGroups(tnode.get(i), groupnodes);
429        }
430      }
431      for (int n = 0; n < groupnodes.size(); n++) {
432        if (groupnodes.get(n).getChildren() == null) {
433          groupnodes.remove(n);
434          n--;
435        } else {
436          Collections.sort(groupnodes.get(n).getChildren(), byLabel);
437        }
438      }
439      node.setChildren(groupnodes);
440    }
441
442    StringBuilder buf = new StringBuilder();
443    node.renderGroupNodesTOC(buf);
444    if (Doclava.samplesNavTree != null) {
445          Doclava.samplesNavTree.setValue("samples_toc_tree", buf.toString());
446    }
447
448  }
449
450  /**
451  * For a given project root node, get the group and then iterate the list of valid
452  * groups looking for a match. If found, append the project to that group node.
453  * Samples that reference a valid sample group tag are added to a list for that
454  * group. Samples declare a sample.group tag in their _index.jd files.
455  */
456  private static List<Node> appendNodeGroups(Node gNode, List<Node> groupnodes) {
457    List<Node> mgrouplist = new ArrayList<Node>();
458    for (int i = 0; i < groupnodes.size(); i++) {
459      if (gNode.getGroup().equals(groupnodes.get(i).getLabel())) {
460        if (groupnodes.get(i).getChildren() == null) {
461          mgrouplist.add(gNode);
462          groupnodes.get(i).setChildren(mgrouplist);
463        } else {
464          groupnodes.get(i).getChildren().add(gNode);
465        }
466        break;
467      }
468    }
469    return groupnodes;
470  }
471
472  /**
473  * Sorts an array of files by type and name (alpha), with manifest always at top.
474  */
475  Comparator<File> byTypeAndName = new Comparator<File>() {
476    public int compare (File one, File other) {
477      if (one.isDirectory() && !other.isDirectory()) {
478        return 1;
479      } else if (!one.isDirectory() && other.isDirectory()) {
480        return -1;
481      } else if ("AndroidManifest.xml".equals(one.getName())) {
482        return -1;
483      } else {
484        return one.compareTo(other);
485      }
486    }
487  };
488
489  /**
490  * Sorts a list of Nodes by label.
491  */
492  public static Comparator<Node> byLabel = new Comparator<Node>() {
493    public int compare(Node one, Node other) {
494      return one.getLabel().compareTo(other.getLabel());
495    }
496  };
497
498  /**
499  * Concatenates dirs that only hold dirs, to simplify nav tree
500  */
501  public static List<Node> squashNodes(List<Node> tnode) {
502    List<Node> list = tnode;
503
504    for(int i = 0; i < list.size(); ++i) {
505      if (("dir".equals(list.get(i).getType())) &&
506          (list.size() == 1) &&
507          (list.get(i).getChildren().get(0).getChildren() != null)) {
508        String thisLabel = list.get(i).getLabel();
509        String childLabel =  list.get(i).getChildren().get(0).getLabel();
510        String newLabel = thisLabel + "/" + childLabel;
511        list.get(i).setLabel(newLabel);
512        list.get(i).setChildren(list.get(i).getChildren().get(0).getChildren());
513      } else {
514        continue;
515      }
516    }
517    return list;
518  }
519
520  public static String convertExtension(String s, String ext) {
521    return s.substring(0, s.lastIndexOf('.')) + ext;
522  }
523
524  /**
525  * Whitelists of valid image/video and source code types.
526  */
527  public static String[] IMAGES = {".png", ".jpg", ".gif"};
528  public static String[] VIDEOS = {".mp4", ".ogv", ".webm"};
529  public static String[] TEMPLATED = {".java", ".xml", ".aidl", ".rs",".txt", ".TXT"};
530
531  public static boolean inList(String s, String[] list) {
532    for (String t : list) {
533      if (s.endsWith(t)) {
534        return true;
535      }
536    }
537    return false;
538  }
539
540  /**
541  * Maps filenames to a set of generic types. Used for displaying files/dirs
542  * in the project view page.
543  */
544  public static String mapTypes(String name) {
545    String type = name.substring(name.lastIndexOf('.') + 1, name.length());
546    if ("xml".equals(type) || "java".equals(type)) {
547      if ("AndroidManifest.xml".equals(name)) type = "manifest";
548      return type;
549    } else {
550      return type = "file";
551    }
552  }
553
554  /**
555  * Validates a source file from a project against restrictions to determine
556  * whether to include the file in the browsable project output.
557  */
558  public boolean isValidFiletype(String name) {
559    if (name.startsWith(".") ||
560        name.startsWith("_") ||
561        "default.properties".equals(name) ||
562        "build.properties".equals(name) ||
563        name.endsWith(".ttf") ||
564        name.endsWith(".gradle") ||
565        name.endsWith(".bat") ||
566        "Android.mk".equals(name)) {
567      return false;
568    } else {
569      return true;
570    }
571  }
572
573  /**
574  * SampleCode variant of NavTree node.
575  */
576  public static class Node {
577    private String mLabel;
578    private String mLink;
579    private String mGroup; // from sample.group in _index.jd
580    private List<Node> mChildren;
581    private String mType;
582
583    private Node(Builder builder) {
584      mLabel = builder.mLabel;
585      mLink = builder.mLink;
586      mGroup = builder.mGroup;
587      mChildren = builder.mChildren;
588      mType = builder.mType;
589    }
590
591    public static class Builder {
592      private String mLabel, mLink, mGroup, mType;
593      private List<Node> mChildren = null;
594      public Builder setLabel(String mLabel) { this.mLabel = mLabel; return this;}
595      public Builder setLink(String mLink) { this.mLink = mLink; return this;}
596      public Builder setGroup(String mGroup) { this.mGroup = mGroup; return this;}
597      public Builder setChildren(List<Node> mChildren) { this.mChildren = mChildren; return this;}
598      public Builder setType(String mType) { this.mType = mType; return this;}
599      public Node build() {return new Node(this);}
600    }
601
602    /**
603    * Renders browsable sample groups and projects to an html list, starting
604    * from the group nodes and then rendering their project nodes and finally their
605    * child dirs and files.
606    */
607    void renderGroupNodesTOC(StringBuilder buf) {
608      List<Node> list = mChildren;
609      if (list == null || list.size() == 0) {
610        return;
611      } else {
612        final int n = list.size();
613        for (int i = 0; i < n; i++) {
614          if (list.get(i).getChildren() == null) {
615            continue;
616          } else {
617            buf.append("<li class=\"nav-section\">");
618            buf.append("<div class=\"nav-section-header\">");
619            buf.append("<a href=\"" + list.get(i).getLink() + "\" title=\""
620                + list.get(i).getLabel() + "\">"
621                + list.get(i).getLabel() + "</a>");
622            buf.append("</div>");
623            buf.append("<ul>");
624            list.get(i).renderProjectNodesTOC(buf);
625          }
626        }
627        buf.append("</ul>");
628        buf.append("</li>");
629      }
630    }
631
632    /**
633    * Renders a list of sample code projects associated with a group node.
634    */
635    void renderProjectNodesTOC(StringBuilder buf) {
636      List<Node> list = mChildren;
637      if (list == null || list.size() == 0) {
638        return;
639      } else {
640        final int n = list.size();
641        for (int i = 0; i < n; i++) {
642          if (list.get(i).getChildren() == null) {
643            continue;
644          } else {
645            buf.append("<li class=\"nav-section\">");
646            buf.append("<div class=\"nav-section-header\">");
647            buf.append("<a href=\"" + list.get(i).getLink() + "\" title=\""
648                + list.get(i).getLabel() + "\">"
649                + list.get(i).getLabel() + "</a>");
650            buf.append("</div>");
651            buf.append("<ul>");
652            list.get(i).renderChildrenToc(buf);
653          }
654        }
655        buf.append("</ul>");
656        buf.append("</li>");
657      }
658    }
659
660    /**
661    * Renders child dirs and files associated with a project node.
662    */
663    void renderChildrenToc(StringBuilder buf) {
664      List<Node> list = mChildren;
665      if (list == null || list.size() == 0) {
666        buf.append("null");
667      } else {
668        final int n = list.size();
669        for (int i = 0; i < n; i++) {
670          if (list.get(i).getChildren() == null) {
671            buf.append("<li>");
672            buf.append("<a href=\"" + list.get(i).getLink() + "\" title=\""
673                + list.get(i).getLabel() + "\">"
674                + list.get(i).getLabel() + "</a>");
675            buf.append("  </li>");
676          } else {
677            buf.append("<li class=\"nav-section sticky\">");
678            buf.append("<div class=\"nav-section-header empty\">");
679            buf.append("<a href=\"#\" onclick=\"return false;\" title=\""
680                + list.get(i).getLabel() + "\">"
681                + list.get(i).getLabel() + "/</a>");
682            buf.append("</div>");
683            buf.append("<ul>");
684            list.get(i).renderChildrenToc(buf);
685          }
686        }
687        buf.append("</ul>");
688        buf.append("</li>");
689      }
690    }
691
692    /**
693    * Node getters and setters
694    */
695    public String getLabel() {
696      return mLabel;
697    }
698
699    public void setLabel(String label) {
700       mLabel = label;
701    }
702
703    public String getLink() {
704      return mLink;
705    }
706
707    public void setLink(String ref) {
708       mLink = ref;
709    }
710
711    public String getGroup() {
712      return mGroup;
713    }
714
715    public void setGroup(String group) {
716      mGroup = group;
717    }
718
719    public List<Node> getChildren() {
720        return mChildren;
721    }
722
723    public void setChildren(List<Node> node) {
724        mChildren = node;
725    }
726
727    public String getType() {
728      return mType;
729    }
730
731    public void setType(String type) {
732      mType = type;
733    }
734  }
735}
736