/*******************************************************************************
* Copyright (c) 2004, 2010 BREDEX GmbH.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BREDEX GmbH - initial API and implementation and/or initial documentation
*******************************************************************************/
package org.eclipse.jubula.rc.common.components;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.eclipse.jubula.rc.common.AUTServerConfiguration;
import org.eclipse.jubula.rc.common.Constants;
import org.eclipse.jubula.rc.common.logger.AutServerLogger;
import org.eclipse.jubula.tools.internal.objects.IComponentIdentifier;
import org.eclipse.jubula.tools.internal.xml.businessmodell.Profile;
/**
* @created Nov 30, 2006
*/
public abstract class FindComponentBP {
/** the logger */
private static final AutServerLogger LOG =
new AutServerLogger(FindComponentBP.class);
/** the factor how strong path equivalenz will increase total equivalenz */
private double m_pathFactor = Constants.PATH_FACTOR;
/** the factor how strong context equivalenz will increase total equivalenz */
private double m_contextFactor = Constants.CONTEXT_FACTOR;
/** the threshold value, when a component will be selected as equivalents */
private double m_thresholdValue = Constants.THRESHOLD_VALUE;
/** the factor how strong name equivalenz will increase total equivalenz */
private double m_nameFactor = Constants.NAME_FACTOR;
/** the current AUT hierarchy */
private AUTHierarchy m_hierarchy;
/**
* Search for the component in the AUT with the given
* <code>componentIdentifier</code>.
*
* @param componentIdentifier the identifier created in object mapping mode
* @param hierarchy the current AUT hierarchy
* @throws IllegalArgumentException if the given identifier is null or <br>
* the hierarchy is not valid: empty or containing null elements
* @return the instance of the component of the AUT
*/
public Object findComponent(
final IComponentIdentifier componentIdentifier,
final AUTHierarchy hierarchy)
throws IllegalArgumentException {
m_hierarchy = hierarchy;
AUTServerConfiguration serverConfig =
AUTServerConfiguration.getInstance();
org.eclipse.jubula.tools.Profile p = componentIdentifier.getProfile();
if (p != null && p instanceof Profile) {
setProfile((Profile) p);
} else {
setProfile(serverConfig.getProfile());
}
// Fuzzy logic parameter
List<String> hierarchyNames = null;
// parameter check
Validate.notNull(componentIdentifier, "The component identifier must not be null."); //$NON-NLS-1$
hierarchyNames = componentIdentifier.getHierarchyNames();
Validate.noNullElements(hierarchyNames,
"The component identifier contains no hierarchy information."); //$NON-NLS-1$
Iterator<HierarchyContainer> allComponents =
new ArrayList<HierarchyContainer>(m_hierarchy.getHierarchyMap()
.values()).iterator();
HierarchyContainer bestMatch = null;
double bestMatchPercentage = 0;
double equivalence = 0;
String suppClassName = componentIdentifier.getSupportedClassName();
final AUTServerConfiguration autServerConf = AUTServerConfiguration
.getInstance();
int numberOfOtherMatchingComponents = 0;
while (allComponents.hasNext()) {
HierarchyContainer current = allComponents.next();
Object currComp = current.getCompID().getComponent();
// check class compatibility first
if (isAvailable(currComp)
&& isSupportedComponent(currComp) && ((suppClassName != null
&& (checkTestableClass(autServerConf, suppClassName, currComp)))
|| componentIdentifier.getComponentName().equals(getCompName(
currComp)))
) {
equivalence = computeEquivalence(componentIdentifier, current);
if (meetsThreshold(equivalence)) {
numberOfOtherMatchingComponents++;
}
if (equivalence > bestMatchPercentage) {
bestMatch = current;
bestMatchPercentage = equivalence;
} else if (equivalence == bestMatchPercentage
&& hierarchy.isInActiveWindow(currComp)) {
bestMatch = current;
}
}
}
Object technicalComponent = null;
if (bestMatch != null
&& meetsThreshold(bestMatchPercentage)) {
technicalComponent = bestMatch.getCompID().getComponent();
}
componentIdentifier.setMatchPercentage(
bestMatchPercentage);
componentIdentifier.setNumberOfOtherMatchingComponents(
numberOfOtherMatchingComponents);
return technicalComponent;
}
/**
* @param currComp the current component
* @return false if component is available.
*/
protected abstract boolean isAvailable(Object currComp);
/**
* Checks if the given current component class or one of its superclass is testable
* with the given suppClassName.
* @param autServerConf the AUTServerConfiguration
* @param suppClassName the class name of the supported component
* @param currComp the current component to test
* @return true if testable, false otherwise.
*/
protected boolean checkTestableClass(
final AUTServerConfiguration autServerConf, String suppClassName,
Object currComp) {
boolean isTestable = false;
try {
isTestable = suppClassName.equals(autServerConf.getTestableClass(
currComp.getClass()).getName());
} catch (IllegalArgumentException iae) { // NOPMD by zeb on 10.04.07 15:13
// The class is not supported
// Do nothing -> isTestable is still false
}
Class superClass = currComp.getClass();
while (!isTestable && superClass != null) {
superClass = superClass.getSuperclass();
try {
isTestable = suppClassName.equals(autServerConf
.getTestableClass(superClass).getName());
} catch (IllegalArgumentException iae) { // NOPMD by zeb on 10.04.07 15:13
// The class is not supported
// Do nothing -> isTestable is still false
}
}
return isTestable;
}
/**
* checks if a component is supported
* @param component Component
* @return boolean
*/
protected boolean isSupportedComponent(Object component) {
try {
AUTServerConfiguration.getInstance().getTestableClass(
component.getClass());
} catch (IllegalArgumentException e) {
return false;
}
return true;
}
/**
* @param profile the profile to set
*/
private void setProfile(Profile profile) {
if (profile != null) {
m_nameFactor = profile.getNameFactor();
m_pathFactor = profile.getPathFactor();
m_contextFactor = profile.getContextFactor();
m_thresholdValue = profile.getThreshold();
}
}
/**
* computes the Equivalence of 2 components
*
* @param compIdent
* ComponentIdentifier
* @param current
* HierarchyContainer
* @return double indicating the equivalence; THIS METHOD uses a shortcut to
* decide continuing the equivalence computing or not - this may
* lead to a "guessed" equivalence which is higher as the real
* equivalence but still lower than the specified threshold
* equivalence.
*/
private double computeEquivalence(IComponentIdentifier compIdent,
HierarchyContainer current) {
double equivalence = 0;
// name equivalence
String name1 = compIdent.getComponentName();
String name2 = current.getName();
double nameEquivalence = 0;
if (isGeneratedName(compIdent.getComponentClassName(), compIdent
.getComponentName())) {
nameEquivalence = getNameEquivalence(name1, name2);
} else {
nameEquivalence = name1.equals(name2) ? 1 : 0;
}
/* check for computing shortcut if equivalence may not meet the
* threshold, although path==1 && context==1 */
equivalence = equivalence(nameEquivalence, 1, 1);
if (!meetsThreshold(equivalence)) {
return equivalence;
}
// path equivalence
double pathEquivalence = getPathEquivalence(compIdent, current);
/* check for computing shortcut if equivalence may not meet the
* threshold, although context==1 */
equivalence = equivalence(nameEquivalence, pathEquivalence, 1);
if (!meetsThreshold(equivalence)) {
return equivalence;
}
// context equivalence
double contextEquivalence = getContextEquivalence(compIdent, current);
// calculate total equivalence
equivalence = equivalence(nameEquivalence,
pathEquivalence,
contextEquivalence);
logEquivalence(compIdent, current, equivalence, nameEquivalence,
pathEquivalence, contextEquivalence);
return equivalence;
}
/**
* @param equivalence the current equivalence
* @return true if it meet's the threshold, false otherwise
*/
private boolean meetsThreshold(double equivalence) {
return (equivalence - m_thresholdValue + 1e-2) > 0;
}
/**
* @param nameE
* the current name equivalence
* @param pathE
* the current path equivalence
* @param contE
* the current context equivalence
* @return the weighted equivalence
*/
private double equivalence(double nameE, double pathE, double contE) {
return m_nameFactor * nameE
+ m_pathFactor * pathE
+ m_contextFactor * contE;
}
/**
* log the results from the equivalence computation
* @param componentIdentifier which component is looked up
* @param current which GUI component is evaluated
* @param equivalence computed equivalence
* @param nameEquivalence equivalence for name
* @param pathEquivalence equivalence for path
* @param contextEquivalence equivalence for context
*/
private void logEquivalence(IComponentIdentifier componentIdentifier,
HierarchyContainer current, double equivalence,
double nameEquivalence, double pathEquivalence,
double contextEquivalence) {
if (LOG.isInfoEnabled()) {
StringBuffer txt = new StringBuffer(500);
txt.append("Equivalence values for Identifier "); //$NON-NLS-1$
txt.append(componentIdentifier.getComponentNameToDisplay());
txt.append(" , matched against "); //$NON-NLS-1$
txt.append(current.getName());
txt.append(" , threshold value: "); //$NON-NLS-1$
txt.append(m_thresholdValue);
txt.append("\n"); //$NON-NLS-1$
txt.append("Equivalence total: "); //$NON-NLS-1$
txt.append(equivalence);
txt.append(" name: "); //$NON-NLS-1$
txt.append(nameEquivalence * m_nameFactor);
txt.append(" path: "); //$NON-NLS-1$
txt.append(pathEquivalence * m_pathFactor);
txt.append(" context: "); //$NON-NLS-1$
txt.append(contextEquivalence * m_contextFactor);
final String txtString = txt.toString();
LOG.info(txtString);
}
}
/**
* computes the Equivalence of 2 names<p>
* Example :<p>
* jButton1 <=> jButton1 = 100.0<p>
* jButton1 <=> jButton2 = 87.5<p>
* jButton1 <=> jTextField1 = 20.0 <p>
* jButton1 <=> jTextField2 = 10.0 <p>
* @param name1 String
* @param name2 String
* @return percentage as double
*/
private double getNameEquivalence(String name1, String name2) {
int diff = StringUtils.getLevenshteinDistance(name1, name2);
double nameEquivalence =
1.0 / Math.max(name1.length(), name2.length())
* (Math.max(name1.length(), name2.length()) - diff);
return nameEquivalence;
}
/**
* @param comp ComponentIdentifier
* @param hierarchyContainer SwtHierarchyContainer
* @return percentage as double
*/
private double getPathEquivalence(IComponentIdentifier comp,
HierarchyContainer hierarchyContainer) {
if ((comp.getHierarchyNames() == null)
|| (comp.getHierarchyNames().size() == 0)) {
return 0;
}
List<String> l1 = comp.getHierarchyNames().subList(
0, comp.getHierarchyNames().size() - 1);
List<String> l2 = new ArrayList<String>();
HierarchyContainer iter = hierarchyContainer.getPrnt();
while (iter != null) {
l2.add(0, iter.getName());
iter = iter.getPrnt();
}
double pathEquivalence = 0;
if (l1.size() == 0 && l2.size() == 0) {
pathEquivalence = 1;
} else {
int diff = getLevenshteinListDistanceImp(l1, l2);
pathEquivalence =
1.0 / Math.max(l1.size(), l2.size())
* (Math.max(l1.size(), l2.size()) - diff);
}
return pathEquivalence;
}
/**
* @param comp ComponentIdentifier
* @param hierarchyContainer HierarchyContainer
* @return percentage as double
*/
private double getContextEquivalence(IComponentIdentifier comp,
HierarchyContainer hierarchyContainer) {
List<String> compNeighbours = comp.getNeighbours();
List<String> compContext = m_hierarchy.getComponentContext(
hierarchyContainer.getCompID().getComponent());
Collections.sort(compNeighbours);
Collections.sort(compContext);
double contextEquivalence = 0;
final int compNeighboursSize = compNeighbours.size();
final int compContextSize = compContext.size();
if (compNeighboursSize == 0 && compContextSize == 0) {
contextEquivalence = 1;
} else {
int diff = getLevenshteinListDistanceImp(compNeighbours,
compContext);
contextEquivalence =
1.0 / Math.max(compNeighboursSize, compContextSize)
* (Math.max(compNeighboursSize, compContextSize) - diff);
}
return contextEquivalence;
}
/**
* Find the Levenshtein distance between two Lists. Heavily based on the
* {@link StringUtils#getLevenshteinDistance(String, String) Apache Commons
* implementation} for Strings.
*
* @see StringUtils#getLevenshteinDistance(String, String)
* @param s The first List. Must not be <code>null</code>.
* @param t The second List. Must not be <code>null</code>.
* @return result distance
*/
private int getLevenshteinListDistanceImp (List<String> s, List<String> t) {
if (s == null || t == null) {
throw new IllegalArgumentException("Lists must not be null"); //$NON-NLS-1$
}
/*
The difference between this impl. and the previous is that, rather
than creating and retaining a matrix of size s.length()+1 by t.length()+1, //$NON-NLS-1$
we maintain two single-dimensional arrays of length s.length()+1. The first, d, //$NON-NLS-1$
is the 'current working' distance array that maintains the newest distance cost //$NON-NLS-1$
counts as we iterate through the characters of String s. Each time we increment //$NON-NLS-1$
the index of String t we are comparing, d is copied to p, the second int[]. Doing so //$NON-NLS-1$
allows us to retain the previous cost counts as required by the algorithm (taking //$NON-NLS-1$
the minimum of the cost count to the left, up one, and diagonally up and to the left //$NON-NLS-1$
of the current cost count being calculated). (Note that the arrays aren't really //$NON-NLS-1$
copied anymore, just switched...this is clearly much better than cloning an array //$NON-NLS-1$
or doing a System.arraycopy() each time through the outer loop.) //$NON-NLS-1$
Effectively, the difference between the two implementations is this one does not //$NON-NLS-1$
cause an out of memory condition when calculating the LD over two very large strings. //$NON-NLS-1$
*/
final int sSize = s.size();
final int tSize = t.size();
if (sSize == 0) {
return tSize;
} else if (tSize == 0) {
return sSize;
}
int p[] = new int[sSize + 1]; //'previous' cost array, horizontally
int d[] = new int[sSize + 1]; // cost array, horizontally
int swapPAndD[]; //placeholder to assist in swapping p and d
// indexes into strings s and t
int sIdx; // iterates through s
int tIdx; // iterates through t
Object currCharOfT;
int cost;
for (sIdx = 0; sIdx <= sSize; sIdx++) {
p[sIdx] = sIdx;
}
for (tIdx = 1; tIdx <= tSize; tIdx++) {
currCharOfT = t.get(tIdx - 1);
d[0] = tIdx;
for (sIdx = 1; sIdx <= sSize; sIdx++) {
cost = s.get(sIdx - 1).equals(currCharOfT) ? 0 : 1;
// minimum of cell to the left+1, to the top+1, diagonally left and up +cost
d[sIdx] = Math.min(Math.min(d[sIdx - 1] + 1,
p[sIdx] + 1), p[sIdx - 1] + cost);
}
// copy current distance counts to 'previous row' distance counts
swapPAndD = p;
p = d;
d = swapPAndD;
}
// our last action in the above loop was to switch d and p, so p now
// actually has the most recent cost counts
return p[sSize];
}
/**
* checks if name is a generated Name
* @param className String
* @param name String
* @return boolean
*/
private boolean isGeneratedName(String className, String name) {
return (name.indexOf(className) != -1);
}
/**
* @param currentComponent the component to get the name for
* @return the component name of the current component
*/
protected abstract String getCompName(Object currentComponent);
}