/* * � Copyright IBM Corp. 2008, 2013 * * 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. */ /* * Author: Maire Kehoe (mkehoe@ie.ibm.com) * Date: 3 Jan 2008 * SpellCheckTest.java */ package com.ibm.xsp.test.framework.registry.annotate; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.ResourceBundle; import org.w3c.dom.CDATASection; import org.w3c.dom.CharacterData; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; import com.ibm.commons.util.StringUtil; import com.ibm.xsp.page.translator.JavaUtil; import com.ibm.xsp.registry.FacesDefinition; import com.ibm.xsp.registry.FacesExtensibleNode; import com.ibm.xsp.registry.FacesLibraryFragment; import com.ibm.xsp.registry.FacesProject; import com.ibm.xsp.registry.FacesProperty; import com.ibm.xsp.registry.FacesPropertyType; import com.ibm.xsp.registry.FacesRegistry; import com.ibm.xsp.registry.FacesRenderKitFragment; import com.ibm.xsp.registry.FacesRendererDefinition; import com.ibm.xsp.registry.FacesSharableRegistry; import com.ibm.xsp.registry.parse.ElementUtil; import com.ibm.xsp.registry.parse.ParseUtil; import com.ibm.xsp.registry.parse.RegistryAnnotater; import com.ibm.xsp.registry.parse.RegistryAnnotaterInfo; import com.ibm.xsp.test.framework.AbstractXspTest; import com.ibm.xsp.test.framework.TestProject; import com.ibm.xsp.test.framework.XspTestUtil; import com.ibm.xsp.test.framework.setup.SkipFileContent; /** * @author Maire Kehoe (mkehoe@ie.ibm.com) * 3 Jan 2008 * Unit: SpellCheckTest.java */ public class SpellCheckTest extends AbstractXspTest { @Override public String getDescription() { return "spell check description and display names for hard-coded common misspellings"; } private List<String[]> toCheckFor; private List<String[]> asEntireWord; private List<String[]> preventExceptInUrls; private String[][]s_toCheckFor = new String[][]{ // whitespace {"\n", "should not be present"}, {"\t", "should not be present"}, {" ", "should not be present"}, // Doc team said these are 2 words, not one word: {"readonly", "should be 'read only'"}, {"checkbox", "should be 'check box'"}, {"Checkbox", "should be 'Check box'"}, // (9.0.0) a search of the Designer help indicates // that "drop-down" is preferred to "drop down" {"drop down","should be 'drop-down'"}, {"Drop Down","should be 'Drop-Down'"}, {"Drop down","should be 'Drop-Down'"}, // Some spelling we were making: {"Javascript", "should be JavaScript"}, {"javascript", "should be JavaScript"}, {"Specifes", "should be Specifies"}, {"specifes", "should be specifies"}, {"Specfies", "should be Specifies"}, {"specfies", "should be specifies"}, {"inital", "should be initial"}, // Grammatical mistake the doc team complained about: {"Specified", "should not be at the start of a sentence"}, // acronyms should be upper case. {"Html", "should be HTML"}, {"xhtml", "should be XHTML"}, {"Xhtml", "should be XHTML"}, {"XHtml", "should be XHTML"}, {"xml", "should be XML"}, {"Xml", "should be XML"}, {"url", "should be URL"}, {"Url", "should be URL"}, {"ajax", "should be AJAX"}, {"Ajax", "should be AJAX"}, {"json", "should be JSON"}, {"Json", "should be JSON"}, {"JSon", "should be JSON"}, // Updated 2012-05-29, this test // used to say "should be XPATH", but // from http://www.w3.org/TR/xpath/ // XPath is not an acronym, and is spelled XPath {"XPATH", "should be XPath"}, {"Xpath", "should be XPath"}, {"xpath", "should be XPath"}, // Product names should be upper case {"domino", "should be Domino"}, {"lotus", "should be Lotus"}, {"dojo", "should be Dojo"}, {"java", "should be Java"}, // for user-friendly-ness {"component", "should be control"}, {"Component", "should be Control"}, // for USA-english vs International english {"'", "should use \" for quotes, and do not use contractions like \"don't\""}, // use of technical terms: {"string","usually should be \"text\""}, {"String","usually should be \"text\""}, // it's two words: {"datasource", "should be 'data source'"}, {"Datasource", "should be 'Data Source'"}, }; private String[][] s_preventExceptInUrls = new String[][]{ // for USA-english vs International english {"/", "should not use \"/\" to mean and/or, as translators and non-USA english speakers don't understand"}, {"html", "should be HTML"}, }; private String[][] s_asEntireWord = new String[][]{ // Doc team say ID is upper case {"Id", "should be ID"}, {"id", "should be ID"}, // acronyms should be upper case. (uri is common within words) {"uri", "should be URI"}, // entire word to prevent fail for "during" {"Uri", "should be URI"}, // spelling mistake {"teh", "should be the"}, {"bat", "should probably be bar"}, // Product names should be upper case {"notes", "should be Notes"}, // entire word to prevent fail for "denotes" // miscellaneous {"ex", "should be e.g."}, }; public void testSpellCheck() throws Exception { String fails = ""; toCheckFor = getToCheckFor(); asEntireWord = getAsEntireWord(); preventExceptInUrls = getPreventExceptInUrls(); FacesSharableRegistry reg = TestProject.createRegistryWithAnnotater(this, new DescriptionDisplayNameAnnotater()); // collect the non-file FacesExtensibleNodes List<NodeInfo> nodes = new ArrayList<NodeInfo>(); for (FacesProject proj : TestProject.getLibProjects(reg, this)) { for (FacesLibraryFragment file : proj.getFiles()) { for (FacesDefinition def : file.getDefs()) { nodes.add(new NodeInfo(def, nodePath(def, null))); for (String propName : def.getDefinedInlinePropertyNames()) { FacesProperty prop = def.getProperty(propName); nodes.add(new NodeInfo(prop, nodePath(def, prop))); if( prop instanceof FacesPropertyType ){ FacesPropertyType propType = (FacesPropertyType) prop; addInnerProps(nodes, propType); } } } for (String kitId : file.getRenderKitIds()) { FacesRenderKitFragment kitFrag = file.getRenderKitFragment(kitId); nodes.add(new NodeInfo(kitFrag, file.getFilePath()+" "+kitFrag.getRenderKitId())); for (FacesRendererDefinition def : kitFrag.getDefs()) { nodes.add(new NodeInfo(def, nodePath(def, null))); for (String propName : def.getDefinedInlinePropertyNames()) { FacesProperty prop = def.getProperty(propName); nodes.add(new NodeInfo(prop, nodePath(def, prop))); if( prop instanceof FacesPropertyType ){ FacesPropertyType propType = (FacesPropertyType) prop; addInnerProps(nodes, propType); } } } } } } for (NodeInfo node : nodes) { String name = (String) node.node.getExtension("display-name"); if( null != name ){ fails += spellCheck(node, name); } String descr = (String) node.node.getExtension("description"); if( null != descr ){ fails += spellCheck(node, descr); } } fails = XspTestUtil.removeMultilineFailSkips(fails, SkipFileContent.concatSkips(getSkips(), this, "testSpellCheck")); if( fails.length() > 0 ){ fail(XspTestUtil.getMultilineFailMessage(fails)); } } protected class NodeInfo{ public FacesExtensibleNode node; public String nodePath; public NodeInfo(FacesExtensibleNode node, String nodePath) { super(); this.node = node; this.nodePath = nodePath; } } protected String[] getSkips(){ return StringUtil.EMPTY_STRING_ARRAY; } /** * Strings whose presence will cause a fail. * <pre> * {badString, reason for fail}, * {badString, reason for fail}, * </pre> * Note, these are test configuration options, not skips. * @return */ protected List<String[]> getToCheckFor(){ List<String[]> list = new ArrayList<String[]>(); list.addAll(Arrays.asList(s_toCheckFor)); return list; } /** * Strings whose presence as a word (rather than a substring) will cause a fail. * Note, these are test configuration options, not skips. * @return */ protected List<String[]> getAsEntireWord(){ List<String[]> list = new ArrayList<String[]>(); list.addAll(Arrays.asList(s_asEntireWord)); return list; } /** * Strings whose presence outside of a URL will cause a fail. * The test's URL detection is not complicated and doesn't conform to any spec. * Note, these are test configuration options, not skips. */ protected List<String[]> getPreventExceptInUrls() { List<String[]> list = new ArrayList<String[]>(); list.addAll(Arrays.asList(s_preventExceptInUrls)); return list; } private void addInnerProps(List<NodeInfo> nodes,FacesPropertyType def) { for (String propName : def.getDefinedInlinePropertyNames()) { FacesProperty prop = def.getProperty(propName); nodes.add(new NodeInfo(prop, nodePath(def, prop))); if( prop instanceof FacesPropertyType ){ FacesPropertyType propType = (FacesPropertyType) prop; addInnerProps(nodes, propType); } } } protected String spellCheck(NodeInfo nodeInfo, String name) { String fails = ""; for (String[] wordAndReason : toCheckFor) { String word = wordAndReason[0]; if( -1 != name.indexOf(word) ){ String reason = wordAndReason[1]; String fail = location(nodeInfo)+" Bad word " + JavaUtil.toJavaString(word) + " (" +reason+"), in: " + fullStr(name); fails += fail + "\n"; } } for (String[] wordAndReason : preventExceptInUrls) { String word = wordAndReason[0]; if( -1 != name.indexOf(word) ){ String nameWithoutUrls = removeUrls(name); if( -1 != nameWithoutUrls.indexOf(word) ){ String reason = wordAndReason[1]; String fail = location(nodeInfo)+" Bad word " + JavaUtil.toJavaString(word) + " (" +reason+"), in: " + fullStr(name); fails += fail + "\n"; } } } for (String[] wordAndReason : asEntireWord) { String word = wordAndReason[0]; int index = name.indexOf(word); if( -1 == index ){ continue; } // contains the word, but check it isn't just a substring of // another word // for each occurrance of the word in the string for (; -1 != index; index = name.indexOf(word, index+1)) { char beforeChar = (0 == index)? ' ' : name.charAt(index-1); boolean atStartOfWord = 0 == index || Character.isWhitespace(beforeChar) || '(' == beforeChar || '[' == beforeChar || '-' == beforeChar; if( ! atStartOfWord ){ continue; } int afterIndex = index + word.length(); boolean atEndOfWord = afterIndex == name.length(); if( ! atEndOfWord ){ char afterChar = name.charAt(afterIndex); atEndOfWord = Character.isWhitespace(afterChar) || '.' == afterChar || ',' == afterChar || ':' == afterChar || '-' == afterChar; } if( !atEndOfWord ){ continue; } String reason = wordAndReason[1]; String fail = location(nodeInfo)+" Bad word " + JavaUtil.toJavaString(word) + " (" +reason+"), in: " + fullStr(name); fails += fail + "\n"; break; // bad word found in this string, ignore other occurrances of the same word. } } return fails; } /** * @param name * @return */ private String removeUrls(String name) { String[] protocols = new String[]{ "http://" }; for (String protocol : protocols) { for (int i = name.indexOf(protocol); i != -1; i = name.indexOf(protocol, i+1)) { // for each occurance of the protocol in the name // yes it's weird but easier for debugging: StringBuilder url = new StringBuilder(protocol); for (int urlCharIndex = i+protocol.length(); urlCharIndex < name.length(); urlCharIndex++) { char nthChar = name.charAt(urlCharIndex); if( (nthChar >= 'a'&& nthChar <='z') || (nthChar >= 'a'&& nthChar <='Z') || (nthChar >= '0'&& nthChar <='p') || nthChar == '/' || nthChar == '-' || nthChar == '.' || nthChar == '_' || nthChar == '#'){ url.append(nthChar); continue; }else{ break; } } String replacement = "[removed URL]"; name = name.replace(url, replacement); } } return name; } public String fullStr(String name) { String escaped = JavaUtil.toJavaString(name); return escaped.substring(1, escaped.length()-1); } /** * @param node * @return */ private String location(NodeInfo node) { return node.nodePath; // if( node instanceof FacesProperty ){ // FacesProperty prop = (FacesProperty)node; // return "prop_" +prop.getName(); // } // if( node instanceof FacesDefinition ){ // FacesDefinition def = (FacesDefinition)node; // return "def_" +def.getId(); // } // return "unknown_"+node; } private String nodePath(FacesDefinition def, FacesProperty prop){ String path = def.getFile().getFilePath()+" "+ ParseUtil.getTagRef(def); if( null != prop ){ path += " "+prop.getName(); } return path; } public static class DescriptionDisplayNameAnnotater implements RegistryAnnotater{ private String[] elemNames = {"description", "display-name"}; private String[] elemValues = new String[elemNames.length]; public void annotate(RegistryAnnotaterInfo info, FacesExtensibleNode parsed, Element elem) { if( isApplicableExtensibleNode(parsed) ){ extractNonTrimValues(elem, elemNames, elemValues); int index = 0; for (String value : elemValues) { if( null != value ){ value = toLocalized(info, value); parsed.setExtension(elemNames[index], value); } index++; } } } /** * @param elem * @param names * @param values */ private void extractNonTrimValues(Element elem, String[] names, String[] values) { // like ElementUtil.extractValues(elem, elemNames, elemValues); // except without calling trim on the values Arrays.fill(values, null); int numberUnSet = names.length; for (Element child : ElementUtil.getChildren(elem)) { int nameIndex = -1; int j = 0; for (String name : names) { if( child.getLocalName().equals(name) ){ nameIndex = j; break; } j++; } if( -1 == nameIndex ){ continue; } values[nameIndex] = getContents(child); if( --numberUnSet == 0 ){ break; } } } private String getContents(Element i) { // should maybe serialze the contents of the element, instead of just // assuming that it only contains text. StringBuffer buffer = new StringBuffer(); // append all the text in the given element appendChildren(i, buffer); return buffer.toString(); } private void appendChildren(Element element, StringBuffer buffer) { NodeList children = element.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if( child instanceof CDATASection || child instanceof Text ){ buffer.append( ((CharacterData)child).getData() ); } if( child instanceof Element ){ appendChildren((Element)child, buffer); } } } protected boolean isApplicableExtensibleNode(FacesExtensibleNode parsed) { // not reading the faces-config-extension description & display-name return !(parsed instanceof FacesLibraryFragment); } private String toLocalized(RegistryAnnotaterInfo info, String value) { // descriptions & display-names may be translated like %key%, // or like %/referencePath/[descr|name]% // See // http://www-10.lotus.com/ldd/ddwiki.nsf/dx/XPages_configuration_file_format#base+designer-extension if( value.length() > 2 && value.charAt(0) == '%' && value.charAt(value.length()-1) == '%'){ String keyOrPath = value.substring(1, value.length() - 1); if( keyOrPath.charAt(0) == '/' ){ // path String[] segments = (keyOrPath.substring(1)).split("/"); if( segments.length == 2 || segments.length == 3 ){ String lastSegment = segments[segments.length - 1]; int segType = "name".equals(lastSegment)? 1 : "descr".equals(lastSegment)? 2 : 0; if( 0 != segType ){ FacesRegistry reg = info.getRegistry(); FacesDefinition def = reg.findDef(segments[0]); FacesExtensibleNode target; if( segments.length == 2 ){ target = def; }else{ // segments.length == 3 FacesProperty prop = null == def? null : def.getProperty(segments[1]); target = prop; } if( null != target ){ String extensionName = segType == 1? "display-name" : "description"; String extensionValue = (String) target.getExtension(extensionName); if( null != extensionValue ){ return extensionValue; } } } } // fall through if can't find def/prop throw new RuntimeException("Cannot find value for path "+keyOrPath); }else{ // key ResourceBundle bundle = info.getResourceBundle(); if( null != bundle ){ String bundleValue = bundle.getString(keyOrPath); if( null != bundleValue ){ return bundleValue; } } throw new RuntimeException("Cannot find value for key "+keyOrPath); } } // non-translated value return value; } } }