/*
* � Copyright IBM Corp. 2011, 2012
*
* 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: 6 Dec 2011
* BooleanPropertyDefaultTest.java
*/
package com.ibm.xsp.test.framework.registry;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import com.ibm.commons.util.StringUtil;
import com.ibm.xsp.registry.FacesCompositeComponentDefinition;
import com.ibm.xsp.registry.FacesDefinition;
import com.ibm.xsp.registry.FacesProperty;
import com.ibm.xsp.registry.FacesSharableRegistry;
import com.ibm.xsp.stylekit.StyleKitImpl;
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.registry.annotate.PropertyTagsAnnotater;
import com.ibm.xsp.test.framework.registry.annotate.SpellCheckTest.DescriptionDisplayNameAnnotater;
import com.ibm.xsp.test.framework.setup.SkipFileContent;
/**
*
* @author Maire Kehoe (mkehoe@ie.ibm.com)
*/
public class BooleanPropertyDefaultTest extends AbstractXspTest {
@Override
public String getDescription() {
// This test was split out from PropertyDefaultValueTest on 2011-12-06
// and was previously split out from PropertiesHaveSettersTest on 2011-08-02.
// Default values other than those expected by the theme handling are a problem
// as they prevent setting that property value using a theme.
// Also they are likely to lead to problems in the renderer's handling
// of computed bindings returning null.
// In 8.5.3, there is a workaround for some of the theme limitations
// as described in SPR#MKEE8EEMS2, but it still won't work by default,
// and users are unlikely to be aware that the workaround mechanism is available.
// The solution described in the SPR is the new baseValue attribute, used like so:
//<control>
// <name>testTheme_text_escape</name>
// <property type="boolean" baseValue="true">
// <name>escape</name>
// <value>#{javascript: false }</value>
// </property>
//</control>
// Which for controls that have themeId="testTheme_text_escape"
// will set the escape property to false. [If you leave out the
// baseValue property the value will not be set because isEscape defaults
// to returning true, so the theme handling thinks
// that the value was set in the xpage source, and doesn't override
// the escape value.]
return "that boolean <property>s have the expected default value of false, when the getter is invoked";
}
public void testPropertyDefaultValue() throws Exception {
String failsStr = "";
// TODO should not need a FacesContext instance with a UIViewRoot,
// but some of the controls are using FacesContext.getCurrentInstance()
// in their constructors - should JUnit test to prevent that.
FacesContext context = TestProject.createFacesContext(this);
context.setViewRoot(TestProject.loadEmptyPage(this, context));
FacesSharableRegistry reg = TestProject.createRegistryWithAnnotater(
this, new PropertyTagsAnnotater(), new DescriptionDisplayNameAnnotater());
List<Object[]> primitiveDefaultSkips = getPrimitiveDefaultSkips(reg);
List<String> alwaysTruePropertyNames = getAlwaysTruePropertyNames();
// for all definitions
for (FacesDefinition def : TestProject.getComponentsAndComplexes(reg, this)) {
Class<?> compClass = def.getJavaClass();
if( def.getJavaClass().isInterface() || isAbstract(def.getJavaClass()) ){
// won't be able to create an instance
continue;
}
// for all properties (including inherited)
boolean attemptedCreateObject = false;
for (String name : def.getPropertyNames()) {
FacesProperty prop = def.getProperty(name);
// only look at boolean properties
if( !boolean.class.equals(prop.getJavaClass()) ){
continue;
}
if( prop.isAttribute() ){
// property will not have a getter
continue;
}
if( def instanceof FacesCompositeComponentDefinition
&& Arrays.binarySearch(StyleKitImpl._customControlBasePropertys, prop.getName()) < 0 ){
// control is a custom control and the property is a custom control definition
// (as opposed to a property inherited from the custom control base)
// These properties do not have getter/setters, as they are set through
// UIIncludeComposite.getPropertyMap()
continue;
}
if( "loaded".equals(prop.getName()) ){
// the "loaded" property is handed by the page loading
// and does not have a corresponding getter
continue;
}
// find the get method
Method getMethod = getIsMethod(compClass, prop);
if( null == getMethod ){
// UIFoo.isBar() not found: no getter to test ValueBinding used
String msg = def.getFile().getFilePath()+" "+XspTestUtil.getAfterLastDot(compClass.getName());
msg += "."+getIsMethodName(prop);
msg += "() not found: no getter to test ValueBinding used";
failsStr += msg+"\n";
continue; // failed.
}
Class<?> declaringClass = getMethod.getDeclaringClass();
// set the VB onto the object
if( attemptedCreateObject ){
// failed to create the object
continue;
}
Object object = null;
try{
object = compClass.newInstance();
}catch( Exception ex ){
if( ex instanceof InvocationTargetException ){
ex = (Exception) ((InvocationTargetException)ex).getCause();
}
ex.printStackTrace();
failsStr += def.getFile().getFilePath()+" "+XspTestUtil.getAfterLastDot(compClass.getName())+
" instance create threw " +ex+'\n';
attemptedCreateObject = true;
continue;
}
// invoke the get method
Boolean defaultValue;
try{
defaultValue = (Boolean) getMethod.invoke(object);
}catch( Exception ex2 ){
// fail
failsStr+= def.getFile().getFilePath()+" "+createFailInvokingUnsetGetter(compClass, getMethod,
declaringClass, ex2)+ '\n';
continue;
}
// prevent primitive default values,
// note, this logic copied from StyleKitImpl.isPropertySet(UIComponent, String)
Boolean expectedRuntimeDefault;
boolean isRenderedProp = "rendered".equals(prop.getName())
&& UIComponent.class.isAssignableFrom(def.getJavaClass());
if( isRenderedProp ){
expectedRuntimeDefault = Boolean.TRUE;
}else{
expectedRuntimeDefault = Boolean.FALSE;
}
Boolean expectedTestDefault = expectedRuntimeDefault;
if( ! expectedTestDefault.booleanValue() ){
// there are property names that we expect to always default to
// true, even though the XPages runtime theme handling cannot
// handle setting those properties in the theme files.
// It is a bad idea to add property names to this list,
// as it means both those properties cannot be set in theme files,
// and also, other instances of the property in controls other
// that your own will be expected to have the property default to true.
// Where possible you should add to the getPrimitiveDefaultSkips list
// instead.
if( alwaysTruePropertyNames.contains(prop.getName()) ){
expectedTestDefault = Boolean.TRUE;
}
}
Object[] descriptionDefaultAndMatch = parseDescriptionDefault(prop);
Boolean descriptionDefault = (null == descriptionDefaultAndMatch)?
null : (Boolean)descriptionDefaultAndMatch[0];
if( null != descriptionDefault && defaultValue != descriptionDefault.booleanValue() ){
failsStr += def.getFile().getFilePath()+" "+toString(declaringClass, getMethod,compClass)
+ " Getter value (" +defaultValue + ") "
+ "not matching description default (" +descriptionDefault+"), "
+"found: " +descriptionDefaultAndMatch[1]+ "\n";
}
Boolean configExpectedValue = null;
// the xsp-config file contains a skip - because the property default
// does not match the theme file handling default.
if( PropertyTagsAnnotater.isTaggedRuntimeDefaultTrue(prop) ){
configExpectedValue = Boolean.TRUE;
}
if( PropertyTagsAnnotater.isTaggedRuntimeDefaultFalse(prop) ){
configExpectedValue = Boolean.FALSE;
}
if( null != configExpectedValue ){
if( ! configExpectedValue.equals(defaultValue) ){
failsStr += def.getFile().getFilePath()+" "+toString(declaringClass, getMethod,compClass)
+ " Bad skip. Property with <tags> runtime-default-? "
+ "not matching actual primitive default value: "
+ defaultValue
+ " (<tags> expects: "
+ configExpectedValue
+ ")\n";
// fall through and check the actual defaultValue
// against the theme handling default value
}else{
if( expectedRuntimeDefault.equals(configExpectedValue) ){
if( expectedTestDefault.booleanValue() && !expectedTestDefault.equals(defaultValue) ){
// This test is configured to expect this property to default to true,
// even though the runtime theme handling expects it to default to false,
// because most other properties with the same name default to true.
// This situation where the property works in line
// with the runtime theme handling is odd, and inconsistent
// but will not cause a fail here because is gives a good
// behavior at runtime.
}else{
failsStr += def.getFile().getFilePath()+" "+toString(declaringClass, getMethod,compClass)
+ " " + prop.getJavaClass().getName()
+ " Unneeded skip. Property with <tags> runtime-default-? matching "
+ "theme handling default value: "
+ expectedRuntimeDefault
+ "\n";
}
}
// <tags>runtime-default-? skips to prevent the JUnit fail below.
continue;
}
}
boolean defaultValueSameAsRuntimeThemeExpected = defaultValue.equals(expectedRuntimeDefault);
boolean testDefaultConfigured = !expectedRuntimeDefault.equals(expectedTestDefault);
boolean testDefaultMatch = testDefaultConfigured && expectedTestDefault.equals(defaultValue);
if( defaultValueSameAsRuntimeThemeExpected ){
if( testDefaultConfigured && ! testDefaultMatch ){
// fail.
if( !isSkipPrimitiveDefault(primitiveDefaultSkips, declaringClass, def.getJavaClass(), prop.getName()) ){
failsStr += def.getFile().getFilePath()+" "+toString(declaringClass, getMethod,compClass)
+ " "
+ "boolean property default problem. "
+ "Test configured with all props named " +prop.getName()
+ " to default to true. This defaults to: "
+ defaultValue
+ "\n";
}
}else{
// pass
}
}else{ // !defaultValueSameAsRuntimeThemeExpected
if( testDefaultConfigured && testDefaultMatch ){
// pass
}else{
// fail.
if( !isSkipPrimitiveDefault(primitiveDefaultSkips, declaringClass, def.getJavaClass(), prop.getName()) ){
if( testDefaultConfigured ){
failsStr += def.getFile().getFilePath()+" "+toString(declaringClass, getMethod,compClass)
+ " "
+ "boolean property default problem. "
+ "Test configured with all props named " +prop.getName()
+ " to default to true. This defaults to: "
+ defaultValue
+ "\n";
}else{
failsStr += def.getFile().getFilePath()+" "+toString(declaringClass, getMethod,compClass)
+ " "
+ "boolean property with unexpected default value: "
+ defaultValue
+ " (expected "
+ expectedRuntimeDefault
+ ")\n";
}
}
}
}
}// end for all properties (including inherited)
} // end for all definitions
for(Object[] skip : primitiveDefaultSkips ){
if( ! isSkipMarkedAsUsed(skip) ){
failsStr += XspTestUtil.getShortClass((Class<?>) skip[1]) + "."
+ skip[0] + " Unused skip for primitive default value "
+ skip[2] + "\n";
}
}
failsStr = XspTestUtil.removeMultilineFailSkips(failsStr,
SkipFileContent.concatSkips(getSkips(), this, "testPropertyDefaultValue"));
if( failsStr.length() > 0 ){
fail(XspTestUtil.getMultilineFailMessage(failsStr));
}
}
/**
* May be overridden in the subclasses to provide
* a hard-coded list of fails to be skipped/ignored.
* @return
*/
protected String[] getSkips(){
return StringUtil.EMPTY_STRING_ARRAY;
}
private static boolean s_computedDefaultSnippets = false;
private static String[] s_genericDefaultSnippets = new String[]{
"defaults to {0}",
"default value is {0}",
"default is {0}",
"{0} by default",
"default this property is {0}",
};
private static String[] s_descriptionTrueSnippets;
private static String[] s_descriptionFalseSnippets;
@SuppressWarnings("unchecked")
private Object[] parseDescriptionDefault(FacesProperty prop) {
String description = (String) prop.getExtension("description");
if( null != description ){
if( ! s_computedDefaultSnippets ){
boolean[] trueAndFalsePrimitives = new boolean[]{true,false};
String[] quoteArr = new String[]{"", "'", "\""};
List<String>[]trueAndFalseSnippets = new List[2];
trueAndFalseSnippets[0] = new ArrayList<String>();
trueAndFalseSnippets[1] = new ArrayList<String>();
for (String genericSnippet : s_genericDefaultSnippets) {
// "defaults to {0}"
int boolIndex = 0;
for (boolean boolValue : trueAndFalsePrimitives) {
// true
for(String quoteType : quoteArr ){
// '
// "defaults to 'true'"
String quotedBool = quoteType+Boolean.toString(boolValue)+quoteType;
String snippet = genericSnippet.replace("{0}", quotedBool);
trueAndFalseSnippets[boolIndex].add(snippet);
char firstSnippetChar = snippet.charAt(0);
if('\'' != firstSnippetChar && '"' != firstSnippetChar ){
// "Defaults to 'true'"
String capitalizedSnippet = Character.toUpperCase(firstSnippetChar)+snippet.substring(1);
trueAndFalseSnippets[boolIndex].add(capitalizedSnippet);
}
}
boolIndex++;
}
}
trueAndFalseSnippets[0].addAll(Arrays.asList(new String[]{
"Default is to",
"Enabled by default",
"present by default",
}));
trueAndFalseSnippets[1].addAll(Arrays.asList(new String[]{
"Default is not to",
"Disabled by default",
}));
s_descriptionTrueSnippets = trueAndFalseSnippets[0].toArray(new String[trueAndFalseSnippets[0].size()]);
s_descriptionFalseSnippets = trueAndFalseSnippets[1].toArray(new String[trueAndFalseSnippets[1].size()]);
}
for (String trueSnippet : s_descriptionTrueSnippets) {
if( description.contains(trueSnippet) ){
return new Object[]{Boolean.TRUE, trueSnippet};
}
}
for (String falseSnippet : s_descriptionFalseSnippets) {
if( description.contains(falseSnippet) ){
return new Object[]{Boolean.FALSE, falseSnippet};
}
}
}
return null;
}
/**
* @param javaClass
* @return
*/
private boolean isAbstract(Class<?> javaClass) {
int modifiersBitFieldValues = javaClass.getModifiers();
int abstractBitFieldOffset = Modifier.ABSTRACT;
// use bitwise AND operator to check if the field value is true in the fields
return 0 != (modifiersBitFieldValues & abstractBitFieldOffset);
}
/**
* @param primitiveDefaultSkips
* @param declaringClass
* @param propName
* @return
*/
private boolean isSkipPrimitiveDefault(
List<Object[]> primitiveDefaultSkips, Class<?> declaringClass, Class<?> actualClass,
String propName) {
// note, if there are skips for the actual class and for the superclass
// that declares the method, use the skip for the actual class, only
// resorting to the declaring class skip if there is no actual class
// skip.
int skipActualClassIndex = -1;
int skipDeclaringClassIndex = -1;
int i = 0;
for (Object[] skip : primitiveDefaultSkips) {
if( propName.equals(skip[0]) ){
if( actualClass.equals(skip[1]) ){
skipActualClassIndex = i;
break;
}
if( declaringClass.equals(skip[1]) ){
skipDeclaringClassIndex = i;
// not break
}
}
i++;
}
int skipIndex = (-1 != skipActualClassIndex)? skipActualClassIndex : skipDeclaringClassIndex;
if(-1 == skipIndex){
return false;
}
markSkipUsed(primitiveDefaultSkips, skipIndex);
return true;
}
private boolean isSkipMarkedAsUsed(Object[] skip) {
return skip.length >=4 && Boolean.TRUE.equals(skip[3]);
}
private void markSkipUsed(List<Object[]> propSkips, int indexPropSkips) {
if( -1 != indexPropSkips ){
Object[] skip = propSkips.get(indexPropSkips);
if( ! isSkipMarkedAsUsed(skip) ){
if( skip.length < 4 ){
skip = XspTestUtil.concat(skip, new Object[4 - skip.length]);
propSkips.set(indexPropSkips, skip);
}
skip[3] = Boolean.TRUE;
}
}
}
private String toString(Class<?> declaringClass, Method getMethod,
Class<?> compClass) {
String msg = XspTestUtil.getShortClass(declaringClass);
msg += "." + getMethod.getName() + "()";
if (declaringClass != compClass) {
msg += "[Called on ";
msg += XspTestUtil.getAfterLastDot(compClass.getName());
msg += "]";
}
return msg;
}
/**
* <pre>
* new Object[][]{
* new Object[]{ String skip0PropName, Class skip0DefClass, Object usedDefaultValue0},
* new Object[]{ String skip1PropName, Class skip1DefClass, Object usedDefaultValue1},
* }
* </pre>
* @param reg
* @return
*/
protected List<Object[]> getPrimitiveDefaultSkips(FacesSharableRegistry reg){
List<Object[]> skips = new ArrayList<Object[]>();
return skips;
}
/**
* There are property names that we expect to always default to
* true, even though the XPages runtime theme handling cannot
* handle setting those properties in the theme files.
* It is a bad idea to add property names to this list,
* as it means both those properties cannot be set in theme files,
* and also, other instances of the property in controls other
* than your own will be expected to have the property default to true.
* Where possible you should add to the getPrimitiveDefaultSkips list
* instead.
* <pre>
* new String[]{
* String skip0PropName,
* String skip1PropName,
* }
* </pre>
* @return
*/
protected List<String> getAlwaysTruePropertyNames(){
List<String> configuredTrueProps = new ArrayList<String>();
return configuredTrueProps;
}
/**
* @param compClass
* @param getMethod
* @param declaringClass
* @param ex2
* @return
*/
private String createFailInvokingUnsetGetter(Class<?> compClass,
Method getMethod, Class<?> declaringClass, Exception exUnwrapped) {
Throwable ex = exUnwrapped;
if( ex instanceof InvocationTargetException ){
ex = ((InvocationTargetException)ex).getCause();
}
// UISelectItemsEx.getValue() getter threw java.lang.NullPointerException
String msg = XspTestUtil.getAfterLastDot(declaringClass.getName());
msg += "." + getMethod.getName() + "()";
if (declaringClass != compClass) {
msg += "[Called on ";
msg += XspTestUtil.getAfterLastDot(compClass.getName());
msg += "]";
}
System.err.println(getClass().getName()
+ ".testPropertyDefaultValue():"
+ " Exception calling " + msg);
ex.printStackTrace();
msg += " getter for unset prop threw "+ ex;
return msg;
}
private Method getIsMethod(Class<?> objectClass, FacesProperty prop) {
String methodName = getIsMethodName(prop);
Method method = getMethod(objectClass, methodName, null);
return method;
}
private String getIsMethodName(FacesProperty prop) {
String propertyName = prop.getName();
String methodName = "is"
+ propertyName.substring(0, 1).toUpperCase()
+ propertyName.substring(1);
return methodName;
}
private static Method getMethod(Class<?> objectClass, String methodName, Class<?>[] parameterTypes) {
try {
return objectClass.getMethod(methodName, parameterTypes);
}
catch (NoSuchMethodException nsme) {
return null;
}
}
}