/* * Copyright (C) 2013 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.doclava; import java.io.*; import java.text.BreakIterator; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.regex.Pattern; import java.util.regex.Matcher; import java.io.File; import com.google.clearsilver.jsilver.data.Data; import org.ccil.cowan.tagsoup.*; import org.xml.sax.XMLReader; import org.xml.sax.InputSource; import org.xml.sax.Attributes; import org.xml.sax.helpers.DefaultHandler; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import javax.xml.transform.dom.DOMResult; import javax.xml.transform.sax.SAXSource; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathFactory; /** * Metadata associated with a specific documentation page. Extracts * metadata based on the page's declared hdf vars (meta.tags and others) * as well as implicit data relating to the page, such as url, type, etc. * Includes a Node class that represents the metadata and lets it attach * to parent/child elements in the tree metadata nodes for all pages. * Node also includes methods for rendering the node tree to a json file * in docs output, which is then used by JavaScript to load metadata * objects into html pages. */ public class PageMetadata { File mSource; String mDest; String mTagList; static boolean sLowercaseTags = true; static boolean sLowercaseKeywords = true; //static String linkPrefix = (Doclava.META_DBG) ? "/" : "http://developer.android.com/"; /** * regex pattern to match javadoc @link and similar tags. Extracts * root symbol to $1. */ private static final Pattern JD_TAG_PATTERN = Pattern.compile("\\{@.*?[\\s\\.\\#]([A-Za-z\\(\\)\\d_]+)(?=\u007D)\u007D"); public PageMetadata(File source, String dest, List taglist) { mSource = source; mDest = dest; if (dest != null) { int len = dest.length(); if (len > 1 && dest.charAt(len - 1) != '/') { mDest = dest + '/'; } else { mDest = dest; } } } /** * Given a list of metadata nodes organized by type, sort the * root nodes by type name and render the types and their child * metadata nodes to a json file in the out dir. * * @param rootTypeNodesList A list of root metadata nodes, each * representing a type and it's member child pages. */ public static void WriteList(List rootTypeNodesList) { Collections.sort(rootTypeNodesList, BY_TYPE_NAME); Node pageMeta = new Node.Builder().setLabel("TOP").setChildren(rootTypeNodesList).build(); StringBuilder buf = new StringBuilder(); // write the taglist to string format pageMeta.renderTypeResources(buf); pageMeta.renderTypesByTag(buf); // write the taglist to js file Data data = Doclava.makeHDF(); data.setValue("reference_tree", buf.toString()); ClearPage.write(data, "jd_lists_unified.cs", "jd_lists_unified.js"); } /** * Extract supported metadata values from a page and add them as * a child node of a root node based on type. Some metadata values * are normalized. Unsupported metadata fields are ignored. See * Node for supported metadata fields and methods for accessing values. * * @param docfile The file from which to extract metadata. * @param dest The output path for the file, used to set link to page. * @param filename The file from which to extract metadata. * @param hdf Data object in which to store the metadata values. * @param tagList The file from which to extract metadata. */ public static void setPageMetadata(String docfile, String dest, String filename, Data hdf, List tagList) { //exclude this page if author does not want it included boolean excludeNode = "true".equals(hdf.getValue("excludeFromSuggestions","")); //check whether summary and image exist and if not, get them from itemprop/markup Boolean needsSummary = "".equals(hdf.getValue("page.metaDescription", "")); Boolean needsImage = "".equals(hdf.getValue("page.image", "")); if ((needsSummary) || (needsImage)) { //try to extract the metadata from itemprop and markup inferMetadata(docfile, hdf, needsSummary, needsImage); } //extract available metadata and set it in a node if (!excludeNode) { Node pageMeta = new Node.Builder().build(); pageMeta.setLabel(getTitleNormalized(hdf, "page.title")); pageMeta.setTitleFriendly(hdf.getValue("page.titleFriendly","")); pageMeta.setSummary(hdf.getValue("page.metaDescription","")); pageMeta.setLink(getPageUrlNormalized(filename)); pageMeta.setGroup(getStringValueNormalized(hdf,"sample.group")); pageMeta.setKeywords(getPageTagsNormalized(hdf, "page.tags")); pageMeta.setTags(getPageTagsNormalized(hdf, "meta.tags")); pageMeta.setImage(getImageUrlNormalized(hdf.getValue("page.image", ""))); pageMeta.setLang(getLangStringNormalized(filename)); pageMeta.setType(getStringValueNormalized(hdf, "page.type")); appendMetaNodeByType(pageMeta, tagList); } } /** * Attempt to infer page metadata based on the contents of the * file. Load and parse the file as a dom tree. Select values * in this order: 1. dom node specifically tagged with * microdata (itemprop). 2. first qualitifed p or img node. * * @param docfile The file from which to extract metadata. * @param hdf Data object in which to store the metadata values. * @param needsSummary Whether to extract summary metadata. * @param needsImage Whether to extract image metadata. */ public static void inferMetadata(String docfile, Data hdf, Boolean needsSummary, Boolean needsImage) { String sum = ""; String imageUrl = ""; String sumFrom = needsSummary ? "none" : "hdf"; String imgFrom = needsImage ? "none" : "hdf"; String filedata = hdf.getValue("commentText", ""); if (Doclava.META_DBG) System.out.println("----- " + docfile + "\n"); try { XPathFactory xpathFac = XPathFactory.newInstance(); XPath xpath = xpathFac.newXPath(); InputStream inputStream = new ByteArrayInputStream(filedata.getBytes()); XMLReader reader = new Parser(); reader.setFeature(Parser.namespacesFeature, false); reader.setFeature(Parser.namespacePrefixesFeature, false); reader.setFeature(Parser.ignoreBogonsFeature, true); Transformer transformer = TransformerFactory.newInstance().newTransformer(); DOMResult result = new DOMResult(); transformer.transform(new SAXSource(reader, new InputSource(inputStream)), result); org.w3c.dom.Node htmlNode = result.getNode(); if (needsSummary) { StringBuilder sumStrings = new StringBuilder(); XPathExpression ItempropDescExpr = xpath.compile("/descendant-or-self::*" + "[@itemprop='description'][1]//text()[string(.)]"); org.w3c.dom.NodeList nodes = (org.w3c.dom.NodeList) ItempropDescExpr.evaluate(htmlNode, XPathConstants.NODESET); if (nodes.getLength() > 0) { for (int i = 0; i < nodes.getLength(); i++) { String tx = nodes.item(i).getNodeValue(); sumStrings.append(tx); sumFrom = "itemprop"; } } else { XPathExpression FirstParaExpr = xpath.compile("//p[not(../../../" + "@class='notice-developers') and not(../@class='sidebox')" + "and not(@class)]//text()"); nodes = (org.w3c.dom.NodeList) FirstParaExpr.evaluate(htmlNode, XPathConstants.NODESET); if (nodes.getLength() > 0) { for (int i = 0; i < nodes.getLength(); i++) { String tx = nodes.item(i).getNodeValue(); sumStrings.append(tx + " "); sumFrom = "markup"; } } } //found a summary string, now normalize it sum = sumStrings.toString().trim(); if ((sum != null) && (!"".equals(sum))) { sum = getSummaryNormalized(sum); } //normalized summary ended up being too short to be meaningful if ("".equals(sum)) { if (Doclava.META_DBG) System.out.println("Warning: description too short! (" + sum.length() + "chars) ...\n\n"); } //summary looks good, store it to the file hdf data hdf.setValue("page.metaDescription", sum); } if (needsImage) { XPathExpression ItempropImageExpr = xpath.compile("//*[@itemprop='image']/@src"); org.w3c.dom.NodeList imgNodes = (org.w3c.dom.NodeList) ItempropImageExpr.evaluate(htmlNode, XPathConstants.NODESET); if (imgNodes.getLength() > 0) { imageUrl = imgNodes.item(0).getNodeValue(); imgFrom = "itemprop"; } else { XPathExpression FirstImgExpr = xpath.compile("//img/@src"); imgNodes = (org.w3c.dom.NodeList) FirstImgExpr.evaluate(htmlNode, XPathConstants.NODESET); if (imgNodes.getLength() > 0) { //iterate nodes looking for valid image url and normalize. for (int i = 0; i < imgNodes.getLength(); i++) { String tx = imgNodes.item(i).getNodeValue(); //qualify and normalize the image imageUrl = getImageUrlNormalized(tx); //this img src did not qualify, keep looking... if ("".equals(imageUrl)) { if (Doclava.META_DBG) System.out.println(" >>>>> Discarded image: " + tx); continue; } else { imgFrom = "markup"; break; } } } } //img src url looks good, store it to the file hdf data hdf.setValue("page.image", imageUrl); } if (Doclava.META_DBG) System.out.println("Image (" + imgFrom + "): " + imageUrl); if (Doclava.META_DBG) System.out.println("Summary (" + sumFrom + "): " + sum.length() + " chars\n\n" + sum + "\n"); return; } catch (Exception e) { if (Doclava.META_DBG) System.out.println(" >>>>> Exception: " + e + "\n"); } } /** * Normalize a comma-delimited, multi-string value. Split on commas, remove * quotes, trim whitespace, optionally make keywords/tags lowercase for * easier matching. * * @param hdf Data object in which the metadata values are stored. * @param tag The hdf var from which the metadata was extracted. * @return A normalized string value for the specified tag. */ public static String getPageTagsNormalized(Data hdf, String tag) { String normTags = ""; StringBuilder tags = new StringBuilder(); String tagList = hdf.getValue(tag, ""); if (tag.equals("meta.tags") && (tagList.equals(""))) { //use keywords as tags if no meta tags are available tagList = hdf.getValue("page.tags", ""); } if (!tagList.equals("")) { tagList = tagList.replaceAll("\"", ""); String[] tagParts = tagList.split(","); for (int iter = 0; iter < tagParts.length; iter++) { tags.append("\""); if (tag.equals("meta.tags") && sLowercaseTags) { tagParts[iter] = tagParts[iter].toLowerCase(); } else if (tag.equals("page.tags") && sLowercaseKeywords) { tagParts[iter] = tagParts[iter].toLowerCase(); } if (tag.equals("meta.tags")) { //tags.append("#"); //to match hashtag format used with yt/blogger resources tagParts[iter] = tagParts[iter].replaceAll(" ",""); } tags.append(tagParts[iter].trim()); tags.append("\""); if (iter < tagParts.length - 1) { tags.append(","); } } } //write this back to hdf to expose through js if (tag.equals("meta.tags")) { hdf.setValue(tag, tags.toString()); } return tags.toString(); } /** * Normalize a string for which only a single value is supported. * Extract the string up to the first comma, remove quotes, remove * any forward-slash prefix, trim any whitespace, optionally make * lowercase for easier matching. * * @param hdf Data object in which the metadata values are stored. * @param tag The hdf var from which the metadata should be extracted. * @return A normalized string value for the specified tag. */ public static String getStringValueNormalized(Data hdf, String tag) { StringBuilder outString = new StringBuilder(); String tagList = hdf.getValue(tag, ""); tagList.replaceAll("\"", ""); if (!tagList.isEmpty()) { int end = tagList.indexOf(","); if (end != -1) { tagList = tagList.substring(0,end); } tagList = tagList.startsWith("/") ? tagList.substring(1) : tagList; if ("sample.group".equals(tag) && sLowercaseTags) { tagList = tagList.toLowerCase(); } outString.append(tagList.trim()); } return outString.toString(); } /** * Normalize a page title. Extract the string, remove quotes, remove * markup, and trim any whitespace. * * @param hdf Data object in which the metadata values are stored. * @param tag The hdf var from which the metadata should be extracted. * @return A normalized string value for the specified tag. */ public static String getTitleNormalized(Data hdf, String tag) { StringBuilder outTitle = new StringBuilder(); String title = hdf.getValue(tag, ""); if (!title.isEmpty()) { title = escapeString(title); if (title.indexOf(""); title = splitTitle[0]; for (int j = 1; j < splitTitle.length; j++) { title.concat(splitTitle[j]); } } outTitle.append(title.trim()); } return outTitle.toString(); } /** * Extract and normalize a page's language string based on the * lowercased dir path. Non-supported langs are ignored and assigned * the default lang string of "en". * * @param filename A path string to the file relative to root. * @return A normalized lang value. */ public static String getLangStringNormalized(String filename) { String[] stripStr = filename.toLowerCase().split("\\/"); String outFrag = "en"; if (stripStr.length > 0) { for (String t : DocFile.DEVSITE_VALID_LANGS) { if ("intl".equals(stripStr[0])) { if (t.equals(stripStr[1])) { outFrag = stripStr[1]; break; } } } } return outFrag; } /** * Normalize a page summary string and truncate as needed. Strings * exceeding max_chars are truncated at the first word boundary * following the max_size marker. Strings smaller than min_chars * are discarded (as they are assumed to be too little context). * * @param s String extracted from the page as it's summary. * @return A normalized string value. */ public static String getSummaryNormalized(String s) { String str = ""; int max_chars = 250; int min_chars = 50; int marker = 0; if (s.length() < min_chars) { return str; } else { str = s.replaceAll("^\"|\"$", ""); str = str.replaceAll("\\s+", " "); str = JD_TAG_PATTERN.matcher(str).replaceAll("$1"); str = escapeString(str); BreakIterator bi = BreakIterator.getWordInstance(); bi.setText(str); if (str.length() > max_chars) { marker = bi.following(max_chars); } else { marker = bi.last(); } str = str.substring(0, marker); str = str.concat("\u2026" ); } return str; } public static String escapeString(String s) { s = s.replaceAll("\"", """); s = s.replaceAll("\'", "'"); s = s.replaceAll("<", "<"); s = s.replaceAll(">", ">"); s = s.replaceAll("/", "/"); return s; } //Disqualify img src urls that include these substrings public static String[] IMAGE_EXCLUDE = {"/triangle-", "favicon","android-logo", "icon_play.png", "robot-tiny"}; public static boolean inList(String s, String[] list) { for (String t : list) { if (s.contains(t)) { return true; } } return false; } /** * Normalize an img src url by removing docRoot and leading * slash for local image references. These are added later * in js to support offline mode and keep path reference * format consistent with hrefs. * * @param url Abs or rel url sourced from img src. * @return Normalized url if qualified, else empty */ public static String getImageUrlNormalized(String url) { String absUrl = ""; // validate to avoid choosing using specific images if ((url != null) && (!url.equals("")) && (!inList(url, IMAGE_EXCLUDE))) { absUrl = url.replace("{@docRoot}", ""); absUrl = absUrl.replaceFirst("^/(?!/)", ""); } return absUrl; } /** * Normalize an href url by removing docRoot and leading * slash for local image references. These are added later * in js to support offline mode and keep path reference * format consistent with hrefs. * * @param url Abs or rel page url sourced from href * @return Normalized url, either abs or rel to root */ public static String getPageUrlNormalized(String url) { String absUrl = ""; if ((url !=null) && (!url.equals(""))) { absUrl = url.replace("{@docRoot}", ""); absUrl = absUrl.replaceFirst("^/(?!/)", ""); } return absUrl; } /** * Given a metadata node, add it as a child of a root node based on its * type. If there is no root node that matches the node's type, create one * and add the metadata node as a child node. * * @param gNode The node to attach to a root node or add as a new root node. * @param rootList The current list of root nodes. * @return The updated list of root nodes. */ public static List appendMetaNodeByType(Node gNode, List rootList) { String nodeTags = gNode.getType(); boolean matched = false; for (Node n : rootList) { if (n.getType().equals(nodeTags)) { //find any matching type node n.getChildren().add(gNode); matched = true; break; // add to the first root node only } // tag did not match } // end rootnodes matching iterator if (!matched) { List mtaglist = new ArrayList(); // list of file objects that have a given type mtaglist.add(gNode); Node tnode = new Node.Builder().setChildren(mtaglist).setType(nodeTags).build(); rootList.add(tnode); } return rootList; } /** * Given a metadata node, add it as a child of a root node based on its * tag. If there is no root node matching the tag, create one for it * and add the metadata node as a child node. * * @param gNode The node to attach to a root node or add as a new root node. * @param rootTagNodesList The current list of root nodes. * @return The updated list of root nodes. */ public static List appendMetaNodeByTagIndex(Node gNode, List rootTagNodesList) { for (int iter = 0; iter < gNode.getChildren().size(); iter++) { if (gNode.getChildren().get(iter).getTags() != null) { List nodeTags = gNode.getChildren().get(iter).getTags(); boolean matched = false; for (String t : nodeTags) { //process each of the meta.tags for (Node n : rootTagNodesList) { if (n.getLabel().equals(t.toString())) { n.getTags().add(String.valueOf(iter)); matched = true; break; // add to the first root node only } // tag did not match } // end rootnodes matching iterator if (!matched) { List mtaglist = new ArrayList(); // list of objects with a given tag mtaglist.add(String.valueOf(iter)); Node tnode = new Node.Builder().setLabel(t.toString()).setTags(mtaglist).build(); rootTagNodesList.add(tnode); } } } } return rootTagNodesList; } public static final Comparator BY_TAG_NAME = new Comparator() { public int compare (Node one, Node other) { return one.getLabel().compareTo(other.getLabel()); } }; public static final Comparator BY_TYPE_NAME = new Comparator() { public int compare (Node one, Node other) { return one.getType().compareTo(other.getType()); } }; /** * A node for storing page metadata. Use Builder.build() to instantiate. */ public static class Node { private String mLabel; // holds page.title or similar identifier private String mTitleFriendly; // title for card or similar use private String mSummary; // Summary for card or similar use private String mLink; //link href for item click private String mGroup; // from sample.group in _index.jd private List mKeywords; // from page.tags private List mTags; // from meta.tags private String mImage; // holds an href, fully qualified or relative to root private List mChildren; private String mLang; private String mType; // can be file, dir, video show, announcement, etc. private Node(Builder builder) { mLabel = builder.mLabel; mTitleFriendly = builder.mTitleFriendly; mSummary = builder.mSummary; mLink = builder.mLink; mGroup = builder.mGroup; mKeywords = builder.mKeywords; mTags = builder.mTags; mImage = builder.mImage; mChildren = builder.mChildren; mLang = builder.mLang; mType = builder.mType; } private static class Builder { private String mLabel, mTitleFriendly, mSummary, mLink, mGroup, mImage, mLang, mType; private List mKeywords = null; private List mTags = null; private List mChildren = null; public Builder setLabel(String mLabel) { this.mLabel = mLabel; return this;} public Builder setTitleFriendly(String mTitleFriendly) { this.mTitleFriendly = mTitleFriendly; return this; } public Builder setSummary(String mSummary) {this.mSummary = mSummary; return this;} public Builder setLink(String mLink) {this.mLink = mLink; return this;} public Builder setGroup(String mGroup) {this.mGroup = mGroup; return this;} public Builder setKeywords(List mKeywords) { this.mKeywords = mKeywords; return this; } public Builder setTags(List mTags) {this.mTags = mTags; return this;} public Builder setImage(String mImage) {this.mImage = mImage; return this;} public Builder setChildren(List mChildren) {this.mChildren = mChildren; return this;} public Builder setLang(String mLang) {this.mLang = mLang; return this;} public Builder setType(String mType) {this.mType = mType; return this;} public Node build() {return new Node(this);} } /** * Render a tree of metadata nodes organized by type. * @param buf Output buffer to render to. */ void renderTypeResources(StringBuilder buf) { List list = mChildren; //list of type rootnodes if (list == null || list.size() == 0) { buf.append("null"); } else { final int n = list.size(); for (int i = 0; i < n; i++) { buf.append("var " + list.get(i).mType.toUpperCase() + "_RESOURCES = ["); list.get(i).renderTypes(buf); //render this type's children buf.append("\n];\n\n"); } } } /** * Render all metadata nodes for a specific type. * @param buf Output buffer to render to. */ void renderTypes(StringBuilder buf) { List list = mChildren; if (list == null || list.size() == 0) { buf.append("nulltype"); } else { final int n = list.size(); for (int i = 0; i < n; i++) { buf.append("\n {\n"); buf.append(" \"title\":\"" + list.get(i).mLabel + "\",\n" ); buf.append(" \"titleFriendly\":\"" + list.get(i).mTitleFriendly + "\",\n" ); buf.append(" \"summary\":\"" + list.get(i).mSummary + "\",\n" ); buf.append(" \"url\":\"" + list.get(i).mLink + "\",\n" ); buf.append(" \"group\":\"" + list.get(i).mGroup + "\",\n" ); list.get(i).renderArrayType(buf, list.get(i).mKeywords, "keywords"); list.get(i).renderArrayType(buf, list.get(i).mTags, "tags"); buf.append(" \"image\":\"" + list.get(i).mImage + "\",\n" ); buf.append(" \"lang\":\"" + list.get(i).mLang + "\",\n" ); buf.append(" \"type\":\"" + list.get(i).mType + "\""); buf.append("\n }"); if (i != n - 1) { buf.append(", "); } } } } /** * Build and render a list of tags associated with each type. * @param buf Output buffer to render to. */ void renderTypesByTag(StringBuilder buf) { List list = mChildren; //list of rootnodes if (list == null || list.size() == 0) { buf.append("null"); } else { final int n = list.size(); for (int i = 0; i < n; i++) { buf.append("var " + list.get(i).mType.toUpperCase() + "_BY_TAG = {"); List mTagList = new ArrayList(); //list of rootnodes mTagList = appendMetaNodeByTagIndex(list.get(i), mTagList); list.get(i).renderTagIndices(buf, mTagList); buf.append("\n};\n\n"); } } } /** * Render a list of tags associated with a type, including the * tag's indices in the type array. * @param buf Output buffer to render to. * @param tagList Node tree of types to render. */ void renderTagIndices(StringBuilder buf, List tagList) { List list = tagList; if (list == null || list.size() == 0) { buf.append(""); } else { final int n = list.size(); for (int i = 0; i < n; i++) { buf.append("\n " + list.get(i).mLabel + ":["); renderArrayValue(buf, list.get(i).mTags); buf.append("]"); if (i != n - 1) { buf.append(", "); } } } } /** * Render key:arrayvalue pair. * @param buf Output buffer to render to. * @param type The list value to render as an arrayvalue. * @param key The key for the pair. */ void renderArrayType(StringBuilder buf, List type, String key) { buf.append(" \"" + key + "\": ["); renderArrayValue(buf, type); buf.append("],\n"); } /** * Render an array value to buf, with special handling of unicode characters. * @param buf Output buffer to render to. * @param type The list value to render as an arrayvalue. */ void renderArrayValue(StringBuilder buf, List type) { List list = type; if (list != null) { final int n = list.size(); for (int i = 0; i < n; i++) { String tagval = list.get(i).toString(); final int L = tagval.length(); for (int t = 0; t < L; t++) { char c = tagval.charAt(t); if (c >= ' ' && c <= '~' && c != '\\') { buf.append(c); } else { buf.append("\\u"); for (int m = 0; m < 4; m++) { char x = (char) (c & 0x000f); if (x > 10) { x = (char) (x - 10 + 'a'); } else { x = (char) (x + '0'); } buf.append(x); c >>= 4; } } } if (i != n - 1) { buf.append(","); } } } } public String getLabel() { return mLabel; } public void setLabel(String label) { mLabel = label; } public String getTitleFriendly() { return mTitleFriendly; } public void setTitleFriendly(String title) { mTitleFriendly = title; } public String getSummary() { return mSummary; } public void setSummary(String summary) { mSummary = summary; } public String getLink() { return mLink; } public void setLink(String ref) { mLink = ref; } public String getGroup() { return mGroup; } public void setGroup(String group) { mGroup = group; } public List getTags() { return mTags; } public void setTags(String tags) { if ("".equals(tags)) { mTags = null; } else { List tagList = new ArrayList(); String[] tagParts = tags.split(","); for (String t : tagParts) { tagList.add(t); } mTags = tagList; } } public List getKeywords() { return mKeywords; } public void setKeywords(String keywords) { if ("".equals(keywords)) { mKeywords = null; } else { List keywordList = new ArrayList(); String[] keywordParts = keywords.split(","); for (String k : keywordParts) { keywordList.add(k); } mKeywords = keywordList; } } public String getImage() { return mImage; } public void setImage(String ref) { mImage = ref; } public List getChildren() { return mChildren; } public void setChildren(List node) { mChildren = node; } public String getLang() { return mLang; } public void setLang(String lang) { mLang = lang; } public String getType() { return mType; } public void setType(String type) { mType = type; } } }