1/*
2 * Copyright (C) 2013 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.io.*;
20import java.text.BreakIterator;
21import java.util.ArrayList;
22import java.util.Collections;
23import java.util.Comparator;
24import java.util.List;
25import java.util.regex.Pattern;
26import java.util.regex.Matcher;
27import java.io.File;
28
29import com.google.clearsilver.jsilver.data.Data;
30
31import org.ccil.cowan.tagsoup.*;
32import org.xml.sax.XMLReader;
33import org.xml.sax.InputSource;
34import org.xml.sax.Attributes;
35import org.xml.sax.helpers.DefaultHandler;
36
37import org.w3c.dom.Node;
38import org.w3c.dom.NodeList;
39
40import javax.xml.transform.dom.DOMResult;
41import javax.xml.transform.sax.SAXSource;
42import javax.xml.transform.Transformer;
43import javax.xml.transform.TransformerFactory;
44import javax.xml.xpath.XPath;
45import javax.xml.xpath.XPathConstants;
46import javax.xml.xpath.XPathExpression;
47import javax.xml.xpath.XPathFactory;
48
49/**
50* Metadata associated with a specific documentation page. Extracts
51* metadata based on the page's declared hdf vars (meta.tags and others)
52* as well as implicit data relating to the page, such as url, type, etc.
53* Includes a Node class that represents the metadata and lets it attach
54* to parent/child elements in the tree metadata nodes for all pages.
55* Node also includes methods for rendering the node tree to a json file
56* in docs output, which is then used by JavaScript to load metadata
57* objects into html pages.
58*/
59
60public class PageMetadata {
61  File mSource;
62  String mDest;
63  String mTagList;
64  static boolean sLowercaseTags = true;
65  static boolean sLowercaseKeywords = true;
66  //static String linkPrefix = (Doclava.META_DBG) ? "/" : "http://developer.android.com/";
67  /**
68   * regex pattern to match javadoc @link and similar tags. Extracts
69   * root symbol to $1.
70   */
71  private static final Pattern JD_TAG_PATTERN =
72      Pattern.compile("\\{@.*?[\\s\\.\\#]([A-Za-z\\(\\)\\d_]+)(?=\u007D)\u007D");
73
74  public PageMetadata(File source, String dest, List<Node> taglist) {
75    mSource = source;
76    mDest = dest;
77
78    if (dest != null) {
79      int len = dest.length();
80      if (len > 1 && dest.charAt(len - 1) != '/') {
81        mDest = dest + '/';
82      } else {
83        mDest = dest;
84      }
85    }
86  }
87
88  /**
89  * Given a list of metadata nodes organized by type, sort the
90  * root nodes by type name and render the types and their child
91  * metadata nodes to a json file in the out dir.
92  *
93  * @param rootTypeNodesList A list of root metadata nodes, each
94  *        representing a type and it's member child pages.
95  */
96  public static void WriteList(List<Node> rootTypeNodesList) {
97
98    Collections.sort(rootTypeNodesList, BY_TYPE_NAME);
99    Node pageMeta = new Node.Builder().setLabel("TOP").setChildren(rootTypeNodesList).build();
100
101    StringBuilder buf = new StringBuilder();
102    // write the taglist to string format
103    pageMeta.renderTypeResources(buf);
104    pageMeta.renderTypesByTag(buf);
105    // write the taglist to js file
106    Data data = Doclava.makeHDF();
107    data.setValue("reference_tree", buf.toString());
108    ClearPage.write(data, "jd_lists_unified.cs", "jd_lists_unified.js");
109  }
110
111  /**
112  * Extract supported metadata values from a page and add them as
113  * a child node of a root node based on type. Some metadata values
114  * are normalized. Unsupported metadata fields are ignored. See
115  * Node for supported metadata fields and methods for accessing values.
116  *
117  * @param docfile The file from which to extract metadata.
118  * @param dest The output path for the file, used to set link to page.
119  * @param filename The file from which to extract metadata.
120  * @param hdf Data object in which to store the metadata values.
121  * @param tagList The file from which to extract metadata.
122  */
123  public static void setPageMetadata(String docfile, String dest, String filename,
124      Data hdf, List<Node> tagList) {
125    //exclude this page if author does not want it included
126    boolean excludeNode = "true".equals(hdf.getValue("excludeFromSuggestions",""));
127
128    //check whether summary and image exist and if not, get them from itemprop/markup
129    Boolean needsSummary = "".equals(hdf.getValue("page.metaDescription", ""));
130    Boolean needsImage = "".equals(hdf.getValue("page.image", ""));
131    if ((needsSummary) || (needsImage)) {
132      //try to extract the metadata from itemprop and markup
133      inferMetadata(docfile, hdf, needsSummary, needsImage);
134    }
135
136    //extract available metadata and set it in a node
137    if (!excludeNode) {
138      Node pageMeta = new Node.Builder().build();
139      pageMeta.setLabel(getTitleNormalized(hdf, "page.title"));
140      pageMeta.setTitleFriendly(hdf.getValue("page.titleFriendly",""));
141      pageMeta.setSummary(hdf.getValue("page.metaDescription",""));
142      pageMeta.setLink(getPageUrlNormalized(filename));
143      pageMeta.setGroup(getStringValueNormalized(hdf,"sample.group"));
144      pageMeta.setKeywords(getPageTagsNormalized(hdf, "page.tags"));
145      pageMeta.setTags(getPageTagsNormalized(hdf, "meta.tags"));
146      pageMeta.setImage(getImageUrlNormalized(hdf.getValue("page.image", "")));
147      pageMeta.setLang(getLangStringNormalized(filename));
148      pageMeta.setType(getStringValueNormalized(hdf, "page.type"));
149      appendMetaNodeByType(pageMeta, tagList);
150    }
151  }
152
153  /**
154  * Attempt to infer page metadata based on the contents of the
155  * file. Load and parse the file as a dom tree. Select values
156  * in this order: 1. dom node specifically tagged with
157  * microdata (itemprop). 2. first qualitifed p or img node.
158  *
159  * @param docfile The file from which to extract metadata.
160  * @param hdf Data object in which to store the metadata values.
161  * @param needsSummary Whether to extract summary metadata.
162  * @param needsImage Whether to extract image metadata.
163  */
164  public static void inferMetadata(String docfile, Data hdf,
165      Boolean needsSummary, Boolean needsImage) {
166    String sum = "";
167    String imageUrl = "";
168    String sumFrom = needsSummary ? "none" : "hdf";
169    String imgFrom = needsImage ? "none" : "hdf";
170    String filedata = hdf.getValue("commentText", "");
171    if (Doclava.META_DBG) System.out.println("----- " + docfile + "\n");
172
173    try {
174      XPathFactory xpathFac = XPathFactory.newInstance();
175      XPath xpath = xpathFac.newXPath();
176      InputStream inputStream = new ByteArrayInputStream(filedata.getBytes());
177      XMLReader reader = new Parser();
178      reader.setFeature(Parser.namespacesFeature, false);
179      reader.setFeature(Parser.namespacePrefixesFeature, false);
180      reader.setFeature(Parser.ignoreBogonsFeature, true);
181
182      Transformer transformer = TransformerFactory.newInstance().newTransformer();
183      DOMResult result = new DOMResult();
184      transformer.transform(new SAXSource(reader, new InputSource(inputStream)), result);
185      org.w3c.dom.Node htmlNode = result.getNode();
186
187      if (needsSummary) {
188        StringBuilder sumStrings = new StringBuilder();
189        XPathExpression ItempropDescExpr = xpath.compile("/descendant-or-self::*"
190            + "[@itemprop='description'][1]//text()[string(.)]");
191        org.w3c.dom.NodeList nodes = (org.w3c.dom.NodeList) ItempropDescExpr.evaluate(htmlNode,
192            XPathConstants.NODESET);
193        if (nodes.getLength() > 0) {
194          for (int i = 0; i < nodes.getLength(); i++) {
195            String tx = nodes.item(i).getNodeValue();
196            sumStrings.append(tx);
197            sumFrom = "itemprop";
198          }
199        } else {
200          XPathExpression FirstParaExpr = xpath.compile("//p[not(../../../"
201              + "@class='notice-developers') and not(../@class='sidebox')"
202              + "and not(@class)]//text()");
203          nodes = (org.w3c.dom.NodeList) FirstParaExpr.evaluate(htmlNode, XPathConstants.NODESET);
204          if (nodes.getLength() > 0) {
205            for (int i = 0; i < nodes.getLength(); i++) {
206              String tx = nodes.item(i).getNodeValue();
207              sumStrings.append(tx + " ");
208              sumFrom = "markup";
209            }
210          }
211        }
212        //found a summary string, now normalize it
213        sum = sumStrings.toString().trim();
214        if ((sum != null) && (!"".equals(sum))) {
215          sum = getSummaryNormalized(sum);
216        }
217        //normalized summary ended up being too short to be meaningful
218        if ("".equals(sum)) {
219           if (Doclava.META_DBG) System.out.println("Warning: description too short! ("
220            + sum.length() + "chars) ...\n\n");
221        }
222        //summary looks good, store it to the file hdf data
223        hdf.setValue("page.metaDescription", sum);
224      }
225      if (needsImage) {
226        XPathExpression ItempropImageExpr = xpath.compile("//*[@itemprop='image']/@src");
227        org.w3c.dom.NodeList imgNodes = (org.w3c.dom.NodeList) ItempropImageExpr.evaluate(htmlNode,
228            XPathConstants.NODESET);
229        if (imgNodes.getLength() > 0) {
230          imageUrl = imgNodes.item(0).getNodeValue();
231          imgFrom = "itemprop";
232        } else {
233          XPathExpression FirstImgExpr = xpath.compile("//img/@src");
234          imgNodes = (org.w3c.dom.NodeList) FirstImgExpr.evaluate(htmlNode, XPathConstants.NODESET);
235          if (imgNodes.getLength() > 0) {
236            //iterate nodes looking for valid image url and normalize.
237            for (int i = 0; i < imgNodes.getLength(); i++) {
238              String tx = imgNodes.item(i).getNodeValue();
239              //qualify and normalize the image
240              imageUrl = getImageUrlNormalized(tx);
241              //this img src did not qualify, keep looking...
242              if ("".equals(imageUrl)) {
243                if (Doclava.META_DBG) System.out.println("    >>>>> Discarded image: " + tx);
244                continue;
245              } else {
246                imgFrom = "markup";
247                break;
248              }
249            }
250          }
251        }
252        //img src url looks good, store it to the file hdf data
253        hdf.setValue("page.image", imageUrl);
254      }
255      if (Doclava.META_DBG) System.out.println("Image (" + imgFrom + "): " + imageUrl);
256      if (Doclava.META_DBG) System.out.println("Summary (" + sumFrom + "): " + sum.length()
257          + " chars\n\n" + sum + "\n");
258      return;
259
260    } catch (Exception e) {
261      if (Doclava.META_DBG) System.out.println("    >>>>> Exception: " + e + "\n");
262    }
263  }
264
265  /**
266  * Normalize a comma-delimited, multi-string value. Split on commas, remove
267  * quotes, trim whitespace, optionally make keywords/tags lowercase for
268  * easier matching.
269  *
270  * @param hdf Data object in which the metadata values are stored.
271  * @param tag The hdf var from which the metadata was extracted.
272  * @return A normalized string value for the specified tag.
273  */
274  public static String getPageTagsNormalized(Data hdf, String tag) {
275
276    String normTags = "";
277    StringBuilder tags = new StringBuilder();
278    String tagList = hdf.getValue(tag, "");
279    if (tag.equals("meta.tags") && (tagList.equals(""))) {
280      //use keywords as tags if no meta tags are available
281      tagList = hdf.getValue("page.tags", "");
282    }
283    if (!tagList.equals("")) {
284      tagList = tagList.replaceAll("\"", "");
285      String[] tagParts = tagList.split(",");
286      for (int iter = 0; iter < tagParts.length; iter++) {
287        tags.append("\"");
288        if (tag.equals("meta.tags") && sLowercaseTags) {
289          tagParts[iter] = tagParts[iter].toLowerCase();
290        } else if (tag.equals("page.tags") && sLowercaseKeywords) {
291          tagParts[iter] = tagParts[iter].toLowerCase();
292        }
293        if (tag.equals("meta.tags")) {
294          //tags.append("#"); //to match hashtag format used with yt/blogger resources
295          tagParts[iter] = tagParts[iter].replaceAll(" ","");
296        }
297        tags.append(tagParts[iter].trim());
298        tags.append("\"");
299        if (iter < tagParts.length - 1) {
300          tags.append(",");
301        }
302      }
303    }
304    //write this back to hdf to expose through js
305    if (tag.equals("meta.tags")) {
306      hdf.setValue(tag, tags.toString());
307    }
308    return tags.toString();
309  }
310
311  /**
312  * Normalize a string for which only a single value is supported.
313  * Extract the string up to the first comma, remove quotes, remove
314  * any forward-slash prefix, trim any whitespace, optionally make
315  * lowercase for easier matching.
316  *
317  * @param hdf Data object in which the metadata values are stored.
318  * @param tag The hdf var from which the metadata should be extracted.
319  * @return A normalized string value for the specified tag.
320  */
321  public static String getStringValueNormalized(Data hdf, String tag) {
322    StringBuilder outString =  new StringBuilder();
323    String tagList = hdf.getValue(tag, "");
324    tagList.replaceAll("\"", "");
325    if (!tagList.isEmpty()) {
326      int end = tagList.indexOf(",");
327      if (end != -1) {
328        tagList = tagList.substring(0,end);
329      }
330      tagList = tagList.startsWith("/") ? tagList.substring(1) : tagList;
331      if ("sample.group".equals(tag) && sLowercaseTags) {
332        tagList = tagList.toLowerCase();
333      }
334      outString.append(tagList.trim());
335    }
336    return outString.toString();
337  }
338
339  /**
340  * Normalize a page title. Extract the string, remove quotes, remove
341  * markup, and trim any whitespace.
342  *
343  * @param hdf Data object in which the metadata values are stored.
344  * @param tag The hdf var from which the metadata should be extracted.
345  * @return A normalized string value for the specified tag.
346  */
347  public static String getTitleNormalized(Data hdf, String tag) {
348    StringBuilder outTitle =  new StringBuilder();
349    String title = hdf.getValue(tag, "");
350    if (!title.isEmpty()) {
351      title = escapeString(title);
352      if (title.indexOf("<span") != -1) {
353        String[] splitTitle = title.split("<span(.*?)</span>");
354        title = splitTitle[0];
355        for (int j = 1; j < splitTitle.length; j++) {
356          title.concat(splitTitle[j]);
357        }
358      }
359      outTitle.append(title.trim());
360    }
361    return outTitle.toString();
362  }
363
364  /**
365  * Extract and normalize a page's language string based on the
366  * lowercased dir path. Non-supported langs are ignored and assigned
367  * the default lang string of "en".
368  *
369  * @param filename A path string to the file relative to root.
370  * @return A normalized lang value.
371  */
372  public static String getLangStringNormalized(String filename) {
373    String[] stripStr = filename.toLowerCase().split("\\/");
374    String outFrag = "en";
375    if (stripStr.length > 0) {
376      for (String t : DocFile.DEVSITE_VALID_LANGS) {
377        if ("intl".equals(stripStr[0])) {
378          if (t.equals(stripStr[1])) {
379            outFrag = stripStr[1];
380            break;
381          }
382        }
383      }
384    }
385    return outFrag;
386  }
387
388  /**
389  * Normalize a page summary string and truncate as needed. Strings
390  * exceeding max_chars are truncated at the first word boundary
391  * following the max_size marker. Strings smaller than min_chars
392  * are discarded (as they are assumed to be too little context).
393  *
394  * @param s String extracted from the page as it's summary.
395  * @return A normalized string value.
396  */
397  public static String getSummaryNormalized(String s) {
398    String str = "";
399    int max_chars = 250;
400    int min_chars = 50;
401    int marker = 0;
402    if (s.length() < min_chars) {
403      return str;
404    } else {
405      str = s.replaceAll("^\"|\"$", "");
406      str = str.replaceAll("\\s+", " ");
407      str = JD_TAG_PATTERN.matcher(str).replaceAll("$1");
408      str = escapeString(str);
409      BreakIterator bi = BreakIterator.getWordInstance();
410      bi.setText(str);
411      if (str.length() > max_chars) {
412        marker = bi.following(max_chars);
413      } else {
414        marker = bi.last();
415      }
416      str = str.substring(0, marker);
417      str = str.concat("\u2026" );
418    }
419    return str;
420  }
421
422  public static String escapeString(String s) {
423    s = s.replaceAll("\"", "&quot;");
424    s = s.replaceAll("\'", "&#39;");
425    s = s.replaceAll("<", "&lt;");
426    s = s.replaceAll(">", "&gt;");
427    s = s.replaceAll("/", "&#47;");
428    return s;
429  }
430
431  //Disqualify img src urls that include these substrings
432  public static String[] IMAGE_EXCLUDE = {"/triangle-", "favicon","android-logo",
433      "icon_play.png", "robot-tiny"};
434
435  public static boolean inList(String s, String[] list) {
436    for (String t : list) {
437      if (s.contains(t)) {
438        return true;
439      }
440    }
441    return false;
442  }
443
444  /**
445  * Normalize an img src url by removing docRoot and leading
446  * slash for local image references. These are added later
447  * in js to support offline mode and keep path reference
448  * format consistent with hrefs.
449  *
450  * @param url Abs or rel url sourced from img src.
451  * @return Normalized url if qualified, else empty
452  */
453  public static String getImageUrlNormalized(String url) {
454    String absUrl = "";
455    // validate to avoid choosing using specific images
456    if ((url != null) && (!url.equals("")) && (!inList(url, IMAGE_EXCLUDE))) {
457      absUrl = url.replace("{@docRoot}", "");
458      absUrl = absUrl.replaceFirst("^/(?!/)", "");
459    }
460    return absUrl;
461  }
462
463  /**
464  * Normalize an href url by removing docRoot and leading
465  * slash for local image references. These are added later
466  * in js to support offline mode and keep path reference
467  * format consistent with hrefs.
468  *
469  * @param url Abs or rel page url sourced from href
470  * @return Normalized url, either abs or rel to root
471  */
472  public static String getPageUrlNormalized(String url) {
473    String absUrl = "";
474    if ((url !=null) && (!url.equals(""))) {
475      absUrl = url.replace("{@docRoot}", "");
476      absUrl = absUrl.replaceFirst("^/(?!/)", "");
477    }
478    return absUrl;
479  }
480
481  /**
482  * Given a metadata node, add it as a child of a root node based on its
483  * type. If there is no root node that matches the node's type, create one
484  * and add the metadata node as a child node.
485  *
486  * @param gNode The node to attach to a root node or add as a new root node.
487  * @param rootList The current list of root nodes.
488  * @return The updated list of root nodes.
489  */
490  public static List<Node> appendMetaNodeByType(Node gNode, List<Node> rootList) {
491
492    String nodeTags = gNode.getType();
493    boolean matched = false;
494    for (Node n : rootList) {
495      if (n.getType().equals(nodeTags)) {  //find any matching type node
496        n.getChildren().add(gNode);
497        matched = true;
498        break; // add to the first root node only
499      } // tag did not match
500    } // end rootnodes matching iterator
501    if (!matched) {
502      List<Node> mtaglist = new ArrayList<Node>(); // list of file objects that have a given type
503      mtaglist.add(gNode);
504      Node tnode = new Node.Builder().setChildren(mtaglist).setType(nodeTags).build();
505      rootList.add(tnode);
506    }
507    return rootList;
508  }
509
510  /**
511  * Given a metadata node, add it as a child of a root node based on its
512  * tag. If there is no root node matching the tag, create one for it
513  * and add the metadata node as a child node.
514  *
515  * @param gNode The node to attach to a root node or add as a new root node.
516  * @param rootTagNodesList The current list of root nodes.
517  * @return The updated list of root nodes.
518  */
519  public static List<Node> appendMetaNodeByTagIndex(Node gNode, List<Node> rootTagNodesList) {
520
521    for (int iter = 0; iter < gNode.getChildren().size(); iter++) {
522      if (gNode.getChildren().get(iter).getTags() != null) {
523        List<String> nodeTags = gNode.getChildren().get(iter).getTags();
524        boolean matched = false;
525        for (String t : nodeTags) { //process each of the meta.tags
526          for (Node n : rootTagNodesList) {
527            if (n.getLabel().equals(t.toString())) {
528              n.getTags().add(String.valueOf(iter));
529              matched = true;
530              break; // add to the first root node only
531            } // tag did not match
532          } // end rootnodes matching iterator
533          if (!matched) {
534            List<String> mtaglist = new ArrayList<String>(); // list of objects with a given tag
535            mtaglist.add(String.valueOf(iter));
536            Node tnode = new Node.Builder().setLabel(t.toString()).setTags(mtaglist).build();
537            rootTagNodesList.add(tnode);
538          }
539        }
540      }
541    }
542    return rootTagNodesList;
543  }
544
545  public static final Comparator<Node> BY_TAG_NAME = new Comparator<Node>() {
546    public int compare (Node one, Node other) {
547      return one.getLabel().compareTo(other.getLabel());
548    }
549  };
550
551  public static final Comparator<Node> BY_TYPE_NAME = new Comparator<Node>() {
552    public int compare (Node one, Node other) {
553      return one.getType().compareTo(other.getType());
554    }
555  };
556
557  /**
558  * A node for storing page metadata. Use Builder.build() to instantiate.
559  */
560  public static class Node {
561
562    private String mLabel; // holds page.title or similar identifier
563    private String mTitleFriendly; // title for card or similar use
564    private String mSummary; // Summary for card or similar use
565    private String mLink; //link href for item click
566    private String mGroup; // from sample.group in _index.jd
567    private List<String> mKeywords; // from page.tags
568    private List<String> mTags; // from meta.tags
569    private String mImage; // holds an href, fully qualified or relative to root
570    private List<Node> mChildren;
571    private String mLang;
572    private String mType; // can be file, dir, video show, announcement, etc.
573
574    private Node(Builder builder) {
575      mLabel = builder.mLabel;
576      mTitleFriendly = builder.mTitleFriendly;
577      mSummary = builder.mSummary;
578      mLink = builder.mLink;
579      mGroup = builder.mGroup;
580      mKeywords = builder.mKeywords;
581      mTags = builder.mTags;
582      mImage = builder.mImage;
583      mChildren = builder.mChildren;
584      mLang = builder.mLang;
585      mType = builder.mType;
586    }
587
588    private static class Builder {
589      private String mLabel, mTitleFriendly, mSummary, mLink, mGroup, mImage, mLang, mType;
590      private List<String> mKeywords = null;
591      private List<String> mTags = null;
592      private List<Node> mChildren = null;
593      public Builder setLabel(String mLabel) { this.mLabel = mLabel; return this;}
594      public Builder setTitleFriendly(String mTitleFriendly) {
595        this.mTitleFriendly = mTitleFriendly; return this;
596      }
597      public Builder setSummary(String mSummary) {this.mSummary = mSummary; return this;}
598      public Builder setLink(String mLink) {this.mLink = mLink; return this;}
599      public Builder setGroup(String mGroup) {this.mGroup = mGroup; return this;}
600      public Builder setKeywords(List<String> mKeywords) {
601        this.mKeywords = mKeywords; return this;
602      }
603      public Builder setTags(List<String> mTags) {this.mTags = mTags; return this;}
604      public Builder setImage(String mImage) {this.mImage = mImage; return this;}
605      public Builder setChildren(List<Node> mChildren) {this.mChildren = mChildren; return this;}
606      public Builder setLang(String mLang) {this.mLang = mLang; return this;}
607      public Builder setType(String mType) {this.mType = mType; return this;}
608      public Node build() {return new Node(this);}
609    }
610
611    /**
612    * Render a tree of metadata nodes organized by type.
613    * @param buf Output buffer to render to.
614    */
615    void renderTypeResources(StringBuilder buf) {
616      List<Node> list = mChildren; //list of type rootnodes
617      if (list == null || list.size() == 0) {
618        buf.append("null");
619      } else {
620        final int n = list.size();
621        for (int i = 0; i < n; i++) {
622          buf.append("var " + list.get(i).mType.toUpperCase() + "_RESOURCES = [");
623          list.get(i).renderTypes(buf); //render this type's children
624          buf.append("\n];\n\n");
625        }
626      }
627    }
628    /**
629    * Render all metadata nodes for a specific type.
630    * @param buf Output buffer to render to.
631    */
632    void renderTypes(StringBuilder buf) {
633      List<Node> list = mChildren;
634      if (list == null || list.size() == 0) {
635        buf.append("nulltype");
636      } else {
637        final int n = list.size();
638        for (int i = 0; i < n; i++) {
639          buf.append("\n      {\n");
640          buf.append("        \"title\":\"" + list.get(i).mLabel + "\",\n" );
641          buf.append("        \"titleFriendly\":\"" + list.get(i).mTitleFriendly + "\",\n" );
642          buf.append("        \"summary\":\"" + list.get(i).mSummary + "\",\n" );
643          buf.append("        \"url\":\"" + list.get(i).mLink + "\",\n" );
644          buf.append("        \"group\":\"" + list.get(i).mGroup + "\",\n" );
645          list.get(i).renderArrayType(buf, list.get(i).mKeywords, "keywords");
646          list.get(i).renderArrayType(buf, list.get(i).mTags, "tags");
647          buf.append("        \"image\":\"" + list.get(i).mImage + "\",\n" );
648          buf.append("        \"lang\":\"" + list.get(i).mLang + "\",\n" );
649          buf.append("        \"type\":\"" + list.get(i).mType + "\"");
650          buf.append("\n      }");
651          if (i != n - 1) {
652            buf.append(", ");
653          }
654        }
655      }
656    }
657
658    /**
659    * Build and render a list of tags associated with each type.
660    * @param buf Output buffer to render to.
661    */
662    void renderTypesByTag(StringBuilder buf) {
663      List<Node> list = mChildren; //list of rootnodes
664      if (list == null || list.size() == 0) {
665        buf.append("null");
666      } else {
667        final int n = list.size();
668        for (int i = 0; i < n; i++) {
669        buf.append("var " + list.get(i).mType.toUpperCase() + "_BY_TAG = {");
670        List<Node> mTagList = new ArrayList(); //list of rootnodes
671        mTagList = appendMetaNodeByTagIndex(list.get(i), mTagList);
672        list.get(i).renderTagIndices(buf, mTagList);
673          buf.append("\n};\n\n");
674        }
675      }
676    }
677
678    /**
679    * Render a list of tags associated with a type, including the
680    * tag's indices in the type array.
681    * @param buf Output buffer to render to.
682    * @param tagList Node tree of types to render.
683    */
684    void renderTagIndices(StringBuilder buf, List<Node> tagList) {
685      List<Node> list = tagList;
686      if (list == null || list.size() == 0) {
687        buf.append("");
688      } else {
689        final int n = list.size();
690        for (int i = 0; i < n; i++) {
691          buf.append("\n    " + list.get(i).mLabel + ":[");
692          renderArrayValue(buf, list.get(i).mTags);
693          buf.append("]");
694          if (i != n - 1) {
695            buf.append(", ");
696          }
697        }
698      }
699    }
700
701    /**
702    * Render key:arrayvalue pair.
703    * @param buf Output buffer to render to.
704    * @param type The list value to render as an arrayvalue.
705    * @param key The key for the pair.
706    */
707    void renderArrayType(StringBuilder buf, List<String> type, String key) {
708      buf.append("        \"" + key + "\": [");
709      renderArrayValue(buf, type);
710      buf.append("],\n");
711    }
712
713    /**
714    * Render an array value to buf, with special handling of unicode characters.
715    * @param buf Output buffer to render to.
716    * @param type The list value to render as an arrayvalue.
717    */
718    void renderArrayValue(StringBuilder buf, List<String> type) {
719      List<String> list = type;
720      if (list != null) {
721        final int n = list.size();
722        for (int i = 0; i < n; i++) {
723          String tagval = list.get(i).toString();
724          final int L = tagval.length();
725          for (int t = 0; t < L; t++) {
726            char c = tagval.charAt(t);
727            if (c >= ' ' && c <= '~' && c != '\\') {
728              buf.append(c);
729            } else {
730              buf.append("\\u");
731              for (int m = 0; m < 4; m++) {
732                char x = (char) (c & 0x000f);
733                if (x > 10) {
734                  x = (char) (x - 10 + 'a');
735                } else {
736                  x = (char) (x + '0');
737                }
738                buf.append(x);
739                c >>= 4;
740              }
741            }
742          }
743          if (i != n - 1) {
744            buf.append(",");
745          }
746        }
747      }
748    }
749
750    public String getLabel() {
751      return mLabel;
752    }
753
754    public void setLabel(String label) {
755       mLabel = label;
756    }
757
758    public String getTitleFriendly() {
759      return mTitleFriendly;
760    }
761
762    public void setTitleFriendly(String title) {
763       mTitleFriendly = title;
764    }
765
766    public String getSummary() {
767      return mSummary;
768    }
769
770    public void setSummary(String summary) {
771       mSummary = summary;
772    }
773
774    public String getLink() {
775      return mLink;
776    }
777
778    public void setLink(String ref) {
779       mLink = ref;
780    }
781
782    public String getGroup() {
783      return mGroup;
784    }
785
786    public void setGroup(String group) {
787      mGroup = group;
788    }
789
790    public List<String> getTags() {
791        return mTags;
792    }
793
794    public void setTags(String tags) {
795      if ("".equals(tags)) {
796        mTags = null;
797      } else {
798        List<String> tagList = new ArrayList();
799        String[] tagParts = tags.split(",");
800
801        for (String t : tagParts) {
802          tagList.add(t);
803        }
804        mTags = tagList;
805      }
806    }
807
808    public List<String> getKeywords() {
809        return mKeywords;
810    }
811
812    public void setKeywords(String keywords) {
813      if ("".equals(keywords)) {
814        mKeywords = null;
815      } else {
816        List<String> keywordList = new ArrayList();
817        String[] keywordParts = keywords.split(",");
818
819        for (String k : keywordParts) {
820          keywordList.add(k);
821        }
822        mKeywords = keywordList;
823      }
824    }
825
826    public String getImage() {
827        return mImage;
828    }
829
830    public void setImage(String ref) {
831       mImage = ref;
832    }
833
834    public List<Node> getChildren() {
835        return mChildren;
836    }
837
838    public void setChildren(List<Node> node) {
839        mChildren = node;
840    }
841
842    public String getLang() {
843      return mLang;
844    }
845
846    public void setLang(String lang) {
847      mLang = lang;
848    }
849
850    public String getType() {
851      return mType;
852    }
853
854    public void setType(String type) {
855      mType = type;
856    }
857  }
858}
859