/*
* � Copyright IBM Corp. 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: 2 Aug 2011
* PropertyDefaultValue.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.HashMap;
import java.util.List;
import java.util.Map;
import javax.faces.component.UIViewRoot;
import com.ibm.commons.util.StringUtil;
import com.ibm.xsp.registry.FacesComponentDefinition;
import com.ibm.xsp.registry.FacesCompositeComponentDefinition;
import com.ibm.xsp.registry.FacesContainerProperty;
import com.ibm.xsp.registry.FacesDefinition;
import com.ibm.xsp.registry.FacesMethodBindingProperty;
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.setup.SkipFileContent;
/**
*
* @author Maire Kehoe (mkehoe@ie.ibm.com)
*/
public class PropertyDefaultValueTest extends AbstractXspTest {
@Override
public String getDescription() {
// Note this was tested as part of PropertiesHaveSettersTest, until
// 2011-08-02, when it was extracted to this separate test.
// 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.
// 2011-12-06: the boolean propertys are now tested in BooleanPropertyDefaultTest
return "that <property>s have the expected default value of null, or the primitive defaults, when the getter is invoked";
}
private static Map<Class<?>, Object> primitiveToThemeDefaultValue;
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.
TestProject.createFacesContext(this).setViewRoot(new UIViewRoot());
FacesSharableRegistry reg = TestProject.createRegistryWithAnnotater(
this, new PropertyTagsAnnotater());
List<Object[]> nonNullDefaultSkips = getNonNullDefaultSkips(reg);
List<Object[]> primitiveDefaultSkips = getPrimitiveDefaultSkips(reg);
// 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);
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;
}
FacesProperty itemProp = prop;
if( prop instanceof FacesContainerProperty ){
FacesContainerProperty container = (FacesContainerProperty)prop;
if (container.isCollection()
&& container.getCollectionAddMethod() != null) {
// if there's an addMethod don't check setValueBinding
continue;
}
prop = container.getItemProperty();
}
if( itemProp instanceof FacesMethodBindingProperty ){
// method bindings do not support setValueBinding
continue;
}
if( boolean.class.equals( itemProp.getJavaClass()) ){
// booleans are checked in BooleanPropertyDefaultTest
continue;
}
// find the get method
Method getMethod = getGetMethod(compClass, prop);
if( null == getMethod ){
// UIPagerEx.getLang() not found: no getter to test ValueBinding used
String msg = def.getFile().getFilePath()+" "+XspTestUtil.getAfterLastDot(compClass.getName());
msg += "."+getterName(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;
}
if( "rendererType".equals(prop.getName()) && def instanceof FacesComponentDefinition ){
// Note, rendererType is expected to have a non-null default (tested in ComponentRendererTest)
continue;
}
// invoke the get method
Object defaultValue;
try{
defaultValue = getMethod.invoke(object);
}catch( Exception ex2 ){
// fail
failsStr+= def.getFile().getFilePath()+" "+createFailInvokingUnsetGetter(compClass, getMethod,
declaringClass, ex2)+ '\n';
continue;
}
if( Object.class.isAssignableFrom(prop.getJavaClass()) ){
// See NoStringDefaultsTest. String properties should not have a default value
if (null != defaultValue) {
int stringDefaultSkipIndex = getNullDefaultSkipIndex(nonNullDefaultSkips, def, prop);
if (-1 == stringDefaultSkipIndex) {
// fail
failsStr += def.getFile().getFilePath()+" "+toString(declaringClass, getMethod, compClass)
+ " non-primitive property with non-null default value: "
+ defaultValue + "\n";
}
else {
markSkipUsed(nonNullDefaultSkips, stringDefaultSkipIndex);
Object expectedDefaultValue = nonNullDefaultSkips.get(stringDefaultSkipIndex)[2];
if (!StringUtil.equals(expectedDefaultValue,defaultValue)
&& !equalEmptyArrays(expectedDefaultValue, defaultValue)) {
// fail
// skipped default value changed, should
// update the skips
failsStr += def.getFile().getFilePath()+" "+toString(declaringClass,getMethod, compClass)
+ " skipped string default-value changed. "
+ "Expected <"+ expectedDefaultValue+ "> was <"+ defaultValue
+ "> \n";
}
}
}
}
if( ! Object.class.isAssignableFrom(prop.getJavaClass()) ){
// prevent primitive default values,
// note, this logic copied from StyleKitImpl.isPropertySet(UIComponent, String)
Class<?> propClass = prop.getJavaClass();
Object expectedDefault;
// booleans are checked in BooleanPropertyDefaultTest:
// boolean isRenderedProp = boolean.class.isAssignableFrom(propClass)
// && "rendered".equals(prop.getName())
// && UIComponent.class.isAssignableFrom(def.getJavaClass());
// if( isRenderedProp ){
// expectedDefault = Boolean.TRUE;
// }else{
if( null == primitiveToThemeDefaultValue ){
Object[][] themeDefaults = new Object[][]{
new Object[]{double.class, 0.0},
new Object[]{int.class, 0},
new Object[]{long.class, 0L},
new Object[]{short.class, ((short)0)},
new Object[]{float.class, 0.0F},
// new Object[]{boolean.class, false},
new Object[]{char.class, ((char)0)},
};
Map<Class<?>, Object> map = new HashMap<Class<?>, Object>();
for (Object[] row : themeDefaults) {
map.put((Class<?>) row[0], row[1]);
}
primitiveToThemeDefaultValue = map;
}
expectedDefault = primitiveToThemeDefaultValue.get(propClass);
if( null == expectedDefault){
throw new RuntimeException(""+propClass);
}
// }
Object configExpectedValue = getConfigSkipExpectedValue(prop);
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( expectedDefault.equals(configExpectedValue) ){
failsStr += def.getFile().getFilePath()+" "+toString(declaringClass, getMethod,compClass)
+ " " + prop.getJavaClass().getName()
+ " Unneeded skip. Property with <tags> runtime-default-? matching "
+ "theme handling default value: "
+ expectedDefault
+ "\n";
}
// <tags>runtime-default-? skips to prevent the JUnit fail below.
continue;
}
}
if (!expectedDefault.equals(defaultValue)) {
if( !isSkipPrimitiveDefault(primitiveDefaultSkips, declaringClass, def.getJavaClass(), prop.getName()) ){
failsStr += def.getFile().getFilePath()+" "+toString(declaringClass, getMethod,compClass)
+ " " + propClass.getName()
+ " property with unexpected primitive default value: "
+ defaultValue
+ " (expected "
+ expectedDefault
+ ")\n";
}
}
}
}// end for all properties (including inherited)
} // end for all definitions
for (Object[] skip : nonNullDefaultSkips) {
if( ! isSkipMarkedAsUsed(skip) ){
failsStr += XspTestUtil.getShortClass((Class<?>) skip[1]) + "."
+ skip[0] + " Unused skip for String default value "
+ skip[2] + "\n";
}
}
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(null, this, "testPropertyDefaultValue"));
if( failsStr.length() > 0 ){
fail(XspTestUtil.getMultilineFailMessage(failsStr));
}
}
/**
* @param prop
* @return
*/
private Object getConfigSkipExpectedValue(FacesProperty prop) {
Object configExpectedValue = null;
// the xsp-config file contains a skip - because the property default
// does not match the theme file handling default.
if( boolean.class.equals(prop.getJavaClass()) ){
if( PropertyTagsAnnotater.isTaggedRuntimeDefaultTrue(prop) ){
configExpectedValue = Boolean.TRUE;
}
if( PropertyTagsAnnotater.isTaggedRuntimeDefaultFalse(prop) ){
configExpectedValue = Boolean.FALSE;
}
}
return configExpectedValue;
}
/**
* @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;
}
}
}
/**
* @param object
* @param defaultValue
* @return
*/
private boolean equalEmptyArrays(Object object, Object defaultValue) {
if( null == object && null == defaultValue ){
return true;
}
if( null == object || null == defaultValue ){
return false;
}
if( !object.getClass().isArray() || ! defaultValue.getClass().isArray() ){
return false;
}
if( !object.getClass().getComponentType().equals(defaultValue.getClass().getComponentType()) ) {
return false;
}
// this method doesn't handle primitive arrays
Object[] first = (Object[]) object;
Object[] second = (Object[]) defaultValue;
if( first.length != 0 || second.length != 0 ){
return false;
}
return 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;
}
protected List<Object[]> getNonNullDefaultSkips(FacesSharableRegistry reg) {
List<Object[]> skips = new ArrayList<Object[]>();
return skips;
}
/**
* <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;
}
private int getNullDefaultSkipIndex(
List<Object[]> nonNullDefaultSkips2, FacesDefinition def,
FacesProperty prop) {
String actualProp = prop.getName();
Class<?> actualClass = def.getJavaClass();
int i = 0;
for (Object[] skip : nonNullDefaultSkips2) {
if( actualProp.equals(skip[0]) && actualClass.equals(skip[1]) ){
return i;
}
i++;
}
return -1;
}
/**
* @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 getGetMethod(Class<?> objectClass, FacesProperty prop) {
String methodName = getterName(prop);
Method method = getMethod(objectClass, methodName, null);
return method;
}
private String getterName(FacesProperty prop) {
String propertyName = prop.getName();
Class<?> type = prop.getJavaClass();
String methodName = (boolean.class.equals(type) ? "is" : "get")
+ 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;
}
}
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);
}
}