• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.google.doclava;
18 
19 import java.io.*;
20 import java.text.BreakIterator;
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.Comparator;
24 import java.util.List;
25 import java.util.regex.Pattern;
26 import java.util.regex.Matcher;
27 import java.io.File;
28 
29 import com.google.clearsilver.jsilver.data.Data;
30 
31 import org.ccil.cowan.tagsoup.*;
32 import org.xml.sax.XMLReader;
33 import org.xml.sax.InputSource;
34 import org.xml.sax.Attributes;
35 import org.xml.sax.helpers.DefaultHandler;
36 
37 import org.w3c.dom.Node;
38 import org.w3c.dom.NodeList;
39 
40 import javax.xml.transform.dom.DOMResult;
41 import javax.xml.transform.sax.SAXSource;
42 import javax.xml.transform.Transformer;
43 import javax.xml.transform.TransformerFactory;
44 import javax.xml.xpath.XPath;
45 import javax.xml.xpath.XPathConstants;
46 import javax.xml.xpath.XPathExpression;
47 import 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 
60 public 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 
PageMetadata(File source, String dest, List<Node> taglist)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   */
WriteList(List<Node> rootTypeNodesList)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   */
setPageMetadata(String docfile, String dest, String filename, Data hdf, List<Node> tagList)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   */
inferMetadata(String docfile, Data hdf, Boolean needsSummary, Boolean needsImage)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   */
getPageTagsNormalized(Data hdf, String tag)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   */
getStringValueNormalized(Data hdf, String tag)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   */
getTitleNormalized(Data hdf, String tag)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   */
getLangStringNormalized(String filename)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   */
getSummaryNormalized(String s)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 
escapeString(String s)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 
inList(String s, String[] list)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   */
getImageUrlNormalized(String url)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   */
getPageUrlNormalized(String url)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   */
appendMetaNodeByType(Node gNode, List<Node> rootList)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   */
appendMetaNodeByTagIndex(Node gNode, List<Node> rootTagNodesList)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 
Node(Builder builder)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;
setLabel(String mLabel)593       public Builder setLabel(String mLabel) { this.mLabel = mLabel; return this;}
setTitleFriendly(String mTitleFriendly)594       public Builder setTitleFriendly(String mTitleFriendly) {
595         this.mTitleFriendly = mTitleFriendly; return this;
596       }
setSummary(String mSummary)597       public Builder setSummary(String mSummary) {this.mSummary = mSummary; return this;}
setLink(String mLink)598       public Builder setLink(String mLink) {this.mLink = mLink; return this;}
setGroup(String mGroup)599       public Builder setGroup(String mGroup) {this.mGroup = mGroup; return this;}
setKeywords(List<String> mKeywords)600       public Builder setKeywords(List<String> mKeywords) {
601         this.mKeywords = mKeywords; return this;
602       }
setTags(List<String> mTags)603       public Builder setTags(List<String> mTags) {this.mTags = mTags; return this;}
setImage(String mImage)604       public Builder setImage(String mImage) {this.mImage = mImage; return this;}
setChildren(List<Node> mChildren)605       public Builder setChildren(List<Node> mChildren) {this.mChildren = mChildren; return this;}
setLang(String mLang)606       public Builder setLang(String mLang) {this.mLang = mLang; return this;}
setType(String mType)607       public Builder setType(String mType) {this.mType = mType; return this;}
build()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     */
renderTypeResources(StringBuilder buf)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     */
renderTypes(StringBuilder buf)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     */
renderTypesByTag(StringBuilder buf)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     */
renderTagIndices(StringBuilder buf, List<Node> tagList)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     */
renderArrayType(StringBuilder buf, List<String> type, String key)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     */
renderArrayValue(StringBuilder buf, List<String> type)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 
getLabel()750     public String getLabel() {
751       return mLabel;
752     }
753 
setLabel(String label)754     public void setLabel(String label) {
755        mLabel = label;
756     }
757 
getTitleFriendly()758     public String getTitleFriendly() {
759       return mTitleFriendly;
760     }
761 
setTitleFriendly(String title)762     public void setTitleFriendly(String title) {
763        mTitleFriendly = title;
764     }
765 
getSummary()766     public String getSummary() {
767       return mSummary;
768     }
769 
setSummary(String summary)770     public void setSummary(String summary) {
771        mSummary = summary;
772     }
773 
getLink()774     public String getLink() {
775       return mLink;
776     }
777 
setLink(String ref)778     public void setLink(String ref) {
779        mLink = ref;
780     }
781 
getGroup()782     public String getGroup() {
783       return mGroup;
784     }
785 
setGroup(String group)786     public void setGroup(String group) {
787       mGroup = group;
788     }
789 
getTags()790     public List<String> getTags() {
791         return mTags;
792     }
793 
setTags(String tags)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 
getKeywords()808     public List<String> getKeywords() {
809         return mKeywords;
810     }
811 
setKeywords(String keywords)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 
getImage()826     public String getImage() {
827         return mImage;
828     }
829 
setImage(String ref)830     public void setImage(String ref) {
831        mImage = ref;
832     }
833 
getChildren()834     public List<Node> getChildren() {
835         return mChildren;
836     }
837 
setChildren(List<Node> node)838     public void setChildren(List<Node> node) {
839         mChildren = node;
840     }
841 
getLang()842     public String getLang() {
843       return mLang;
844     }
845 
setLang(String lang)846     public void setLang(String lang) {
847       mLang = lang;
848     }
849 
getType()850     public String getType() {
851       return mType;
852     }
853 
setType(String type)854     public void setType(String type) {
855       mType = type;
856     }
857   }
858 }
859