/**
* Copyright (C) 2008-2010, Squale Project - http://www.squale.org
*
* This file is part of Squale.
*
* Squale is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or any later version.
*
* Squale is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Squale. If not, see <http://www.gnu.org/licenses/>.
*/
package org.squale.squalix.util.parser;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.TreeSet;
import org.apache.bcel.classfile.Utility;
import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.StringUtils;
import org.squale.squalecommon.enterpriselayer.businessobject.component.AbstractComplexComponentBO;
import org.squale.squalecommon.enterpriselayer.businessobject.component.ClassBO;
import org.squale.squalecommon.enterpriselayer.businessobject.component.MethodBO;
import org.squale.squalecommon.enterpriselayer.businessobject.component.PackageBO;
import org.squale.squalecommon.enterpriselayer.businessobject.component.ProjectBO;
/**
* Parse les noms de type java et les remplace par les objets correspondants.<br/> Cette classe est impl�ment�e de
* fa�on � fonctionner avec les nom sp�ciaux remont�s par McCabe.<br/> On ne traite pas les cas McCabe directement dans
* les classes McCabes car cela d�pend du langage.
*/
public class JavaParser
implements LanguageParser
{
/** D�but du nom donn� par McCabe aux classes anonymes */
public static final String MC_CABE_ANONYMOUS_CLASS_NAME = "__Anon_";
/** D�but du nom a donner aux classes anonymes */
public static final String ANONYMOUS_CLASS_NAME = "AnonymousClass";
/** D�but de nom donn� par RSM au premier package d'une Interface */
public static final String RSM_INTERFACE_PACKAGE_NAME = "Interface-";
/** Projet sur lequel est r�alis� l'analyse */
private ProjectBO mProject;
/**
* Ensemble des classes connues Cel� permet de renseigner les noms de classe connues pour permettre une distinction
* entre un package et un nom de classe. McCabe utilise l'op�rateur de qualification "." aussi bien pour une classe
* imbriqu�e que pour un package.
*/
private TreeSet mKnownClasses = new TreeSet();
/**
* Ensemble des classes anonymes remont�es par McCabe.<br/> On utilise une LinkedHashSet pour ne pas avoir de
* doublons et pour l'ordre dans lequel sont ins�r�s les noms soit conserv�.<br/> Ce set va nous permettre de
* num�roter les classes anonymes remont�es par McCabe car on va sauvegarder ces noms de classes lorsqu'une m�thode
* d�finie dans une classe anonyme sera pars�e.<br/> Le rpt de McCabe doit ordonner les m�thodes par ordre
* croissant de leur num�ro de ligne (on suppose ainsi que le compilateur java d�fini l'identifiant d'une classe
* anonyme par le m�me principe).
*/
private LinkedHashSet mAnonymousClasses = new LinkedHashSet();
/**
* Constructeur
*
* @param pProject le projet sur lequel est r�alis� l'analyse
*/
public JavaParser( ProjectBO pProject )
{
mProject = pProject;
}
/**
* Ajout d'une classe connue
*
* @param pClassName nom absolu de la classe
*/
public void addKnownClass( String pClassName )
{
mKnownClasses.add( pClassName );
}
/* ################ D�composition et transformation en objet correspondant ################ */
/**
* {@inheritDoc}
*
* @param pAbsoluteMethodName {@inheritDoc}
* @param pFileName {@inheritDoc}
* @see org.squale.squalix.util.parser.LanguageParser#getMethod(java.lang.String, java.lang.String)
*/
public MethodBO getMethod( String pAbsoluteMethodName, String pFileName )
{
MethodBO methodBO = null;
// On v�rifie que le nom de la m�thode ne fait pas partie des bugs McCabe,
// sinon on retourne null
if ( !isMcCabeBug( pAbsoluteMethodName ) )
{
// Si la m�thode est contenue dans classe anonyme remont�e par McCabe, on l'enregistre
// pour pouvoir d�terminer l'identifiant de cette classe anonyme.
if ( pAbsoluteMethodName.matches( "^.+\\$" + MC_CABE_ANONYMOUS_CLASS_NAME + "[0-9]+_.*" ) )
{
mAnonymousClasses.add( pAbsoluteMethodName );
}
// On nettoie le nom si celui-ci est remont� du rapport McCabe car
// il peut peut ne pas convenir pour le parsing.
String cleanMethodName = clearReportName( pAbsoluteMethodName );
// On r�cup�re le nom de la m�thode
String methodName = getName( cleanMethodName );
methodBO = new MethodBO( methodName );
methodBO.setLongFileName( pFileName );
String absoluteClassName = getAbsoluteClassName( pAbsoluteMethodName );
ClassBO parent = getClass( absoluteClassName );
// On modifie le fichier de la classe car c'est le m�me que celui de la m�thode
parent.setFileName( pFileName );
methodBO.setParent( parent );
}
return methodBO;
}
/**
* @param classNameWithPackage le nom absolu de la classe
* @param relativeFileName le nom du fichier relatif au projet
* @return la classe persist�e
*/
public ClassBO getClass( String classNameWithPackage, String relativeFileName )
{
ClassBO classBO = getClass( classNameWithPackage );
classBO.setFileName( relativeFileName );
return classBO;
}
/**
* {@inheritDoc}
*
* @param pAbsoluteClassName {@inheritDoc}
* @see org.squale.squalix.util.parser.LanguageParser#getClass(java.lang.String)
*/
public ClassBO getClass( String pAbsoluteClassName )
{
int index = 0;
ClassBO classBO = null;
// On v�rifie que le nom de la classe ne fait pas partie des bugs McCabe,
// sinon on retourne null
if ( !isMcCabeBug( pAbsoluteClassName ) )
{
AbstractComplexComponentBO parent = null;
// Si la classe est une classe anonyme remont�e par McCabe, on va recherche
// dans le set l'index d'apparition de cette classe dans le rapport
// des m�thodes McCabe.
if ( pAbsoluteClassName.matches( "^.+\\$" + MC_CABE_ANONYMOUS_CLASS_NAME + "[0-9]+_$" ) )
{
index = indexOfAnonymousClass( pAbsoluteClassName );
}
// On nettoie le nom si celui-ci est remont� du rapport McCabe car
// il peut ne pas convenir pour le parsing.
String cleanClassName = clearReportName( pAbsoluteClassName );
// On r�cup�re le nom de la classe
String className = getName( cleanClassName );
if ( 0 != index )
{
className += index;
}
classBO = new ClassBO( className );
// Si il s'agit d'une classe interne (avec un $) on attache la classe
// � la classe qui la contient non au package directement (pour les
// cas ou deux classes internes porteraient le m�me nom dans des classes
// ayant le m�me package
int lastIndexOfDollard = cleanClassName.lastIndexOf( "$" );
if ( lastIndexOfDollard > 0 )
{
parent = getClass( cleanClassName.substring( 0, lastIndexOfDollard ) );
}
else
{
// On r�cup�re le package associ�
String packageName = getParentName( cleanClassName );
// On attache la classe � un package
// si la classe en a un sinon on l'attache au projet directement.
if ( null != packageName && packageName.trim().length() > 0 )
{
// On doit deviner si le nom du package est un nom de classe
// ou de package.
if ( mKnownClasses.contains( packageName ) )
{
parent = getClass( packageName );
}
else
{
parent = getPackage( packageName );
}
}
else
{
parent = mProject;
}
}
classBO.setParent( parent );
}
return classBO;
}
/**
* Cr�e le package d�sign� par les param�tres.
*
* @param pPackageName le nom absolu du package
* @return le PackageBO d�sign� par les param�tres.
*/
public PackageBO getPackage( String pPackageName )
{
// On r�cup�re le nom relatif du package
String relativePackageName = getPackageName( pPackageName );
PackageBO packageBO = new PackageBO( relativePackageName );
String parentName = getParentName( pPackageName );
// On affecte son package parent ou le projet si il n'a
// pas de package parent.
if ( null == parentName )
{
packageBO.setParent( mProject );
}
else
{
PackageBO parent = getPackage( parentName );
packageBO.setParent( parent );
}
return packageBO;
}
/**
* Retourne la cha�ne pAbsoluteName avant le dernier "." ou null si il n'y a pas de "."
*
* @param pAbsoluteName le nom absolu du fils
* @return le nom absolu du parent
*/
public String getParentName( String pAbsoluteName )
{
String parent = null;
int lastDot = pAbsoluteName.lastIndexOf( "." );
if ( lastDot != -1 )
{
parent = pAbsoluteName.substring( 0, lastDot );
}
return parent;
}
/**
* Retourne le nom enti�rement qualifi� du package de la classe <code>pFullClassName</code><br/> Si la classe n'a
* pas de package, retourne une chaine vide.
*
* @param pFullClassName le nom enti�rement qualifi� de la classe
* @return le nom enti�rement qualifi� du package de la classe.
*/
public String getAbsolutePackage( String pFullClassName )
{
String absolutePackage = getParentName( pFullClassName );
if ( null == absolutePackage )
{
absolutePackage = "";
}
return absolutePackage;
}
/**
* Retourne le nom du package le plus � droite
*
* @param pPackageName le nom absolu du package
* @return le nom du dernier package
*/
private String getPackageName( String pPackageName )
{
String[] splittingPackageName = pPackageName.split( "\\." );
// On supprime le nom rajout� par RSM en cas
return splittingPackageName[splittingPackageName.length - 1].replaceFirst( RSM_INTERFACE_PACKAGE_NAME, "" );
}
/**
* D�compose le nom enti�rement qualifi� d'une classe afin de r�cup�rer son nom relatif.
*
* @param pAbsoluteName le nom enti�rement qualifi� de la classe (ou le nom enti�rement qualifi� de la m�thode)
* @return le nom relatif de la classe (ou de la m�thode sans ses param�tres)
*/
private String getName( String pAbsoluteName )
{
// s'il y a une parenth�se (m�thode) il ne faut pas chercher dans les arguments
int par = pAbsoluteName.lastIndexOf( "(" );
if ( par == -1 )
{
par = pAbsoluteName.length();
}
int firstSharp = pAbsoluteName.indexOf( "#" );
int index = Math.max( pAbsoluteName.lastIndexOf( "$", par ), pAbsoluteName.lastIndexOf( ".", par ) );
// Dans le cas d'un "#", il faut le garder dans le nom contrairement au "$" et"."
if ( firstSharp > index )
{
index = firstSharp - 1;
}
String result = pAbsoluteName.substring( index + 1 );
// On renomme �ventuellement la classe si il s'agit d'une classe anonyme
result = rename( result, pAbsoluteName );
// On nettoie le nom dans le cas d'une classe interne d�finie dans un bloc
// car le compilateur nomme le .class de cette fa�on : ClasseExterner$1ClassInterne.class
// donc on peut avoir des chiffres en d�but de nom de classe.
return clearName( result );
}
/**
* Renomme une classe dans le cas des classes anonymes:<br/>
* <ul>
* <li>Le nom donn� par le compilateur java � une classe anonyme est : nomClass$Id (Id est l'identifiant g�n�r� par
* le compilateur).</li>
* <li>Le nom donn� par McCabe pour une classe anonyme est : __Anon_Id (Id pouvant �tre 001, 002,...).</li>
* </ul>
*
* @param pName le nom d'une classe � renommer
* @param pAbsoluteName le nom abolu de la classe ou de la m�thode.
* @return le nom qu'il faut donner � une classe anonyme ou <code>pName</code> si il ne s'agit pas du nom d'une
* classe anonyme.
*/
private String rename( String pName, String pAbsoluteName )
{
// Si le nom de la classe est un chiffre alors il s'agit d'une class anonyme, on la renomme
// pour avoir une similitude avec McCabe.
String newName = pName;
if ( newName.matches( "[0-9]+" ) )
{
newName = ANONYMOUS_CLASS_NAME + newName;
}
// Si le nom de la classe est le nom d'une classe anonyme remont�e par McCabe, on la renomme
// pour avoir une similitude avec les .class (l'identifiant sera donn� au niveau de getClass()).
if ( newName.startsWith( MC_CABE_ANONYMOUS_CLASS_NAME ) )
{
newName = ANONYMOUS_CLASS_NAME;
}
return newName;
}
/**
* @param pAbsoluteName le nom absolu de la classe anonyme � chercher
* @return l'index + 1 de <code>pAbsoluteName</code> dans le set qui contient toutes les classes anonymes
* r�cup�rer lors du premier passage sur les m�thodes de cette classe anonyme remont�e par McCabe. -1 si le
* nom n'est pas pr�sent dans le set.
*/
private int indexOfAnonymousClass( String pAbsoluteName )
{
int index = -1;
int currentIndex = 1;
// On parcours le set ordonn� jusqu'� trouv� le nom de la classe
for ( Iterator it = mAnonymousClasses.iterator(); it.hasNext() && -1 == index; currentIndex++ )
{
String currentName = (String) it.next();
if ( currentName.startsWith( pAbsoluteName ) )
{
index = currentIndex;
}
}
return index;
}
/**
* Indique si le nom <code>pName</code> correspond � un des bugs de nommage McCabe.<br/> Les nommages consid�r�s
* comme �tant des bugs McCabe sont les suivants:<br/>
* <ul>
* <li>Le nom d'une class ou d'une m�thode se termine par _#Id (Id �tant un chiffre) (ex : methodName_#1). Cela
* peut arriver lorsque deux classes portent le m�me nom dans un m�me projet.</li>
* <li>Le nom d'un package est un chiffre (ex : test.1.blockInnerClass). Cela peut arriver lorsqu'une classe
* interne est d�finie dans un bloc.</li>
* </ul>
*
* @param pName le nom d'une classe ou d'une m�thode � v�rifier
* @return true si le nom remont� correspond � un bug McCabe
*/
private boolean isMcCabeBug( String pName )
{
boolean result = false;
// Cas d'une classe ou d'une m�thode consid�r�e comme dupliqu�e (ex : Name_#1)
String duplicateModule = ".*_#1$";
// Cas d'une classe interne d�finie dans un bloc (ex : test.1.blockInnerClass)
String blockInnerClass = ".*\\.[0-9]\\..*";
result = pName.matches( duplicateModule ) || pName.matches( blockInnerClass );
return result;
}
/**
* Nettoie le nom du composant remont� du rapport McCabe.<br>
* <ul>
* <li>Retire le nom de la classe externe dans le cas d'un nom de m�thode (correspondant d�finie dans une classe
* interne elle-m�me d�finie dans un bloc.</li>
* <li>Retire le nom de la m�thode qui contient la classe anonyme dont le nom correspond � <code>pName</code> (ou
* <code>pName</code> correspond au nom de la m�thode d�finie dans cette classe anonyme).</li>
* </ul>
*
* @param pName le nom a nettoyer.
* @return le nom propre.
*/
public String clearReportName( String pName )
{
String newName = pName;
// On supprime les noms des methodes qui contiennent des classes internes
// remont�es par McCabe (ex : classeExterne.nomMethodeCE`classeInterne.nomMethodeCI)
newName = newName.replaceAll( "\\.[^\\.]+`", "." );
// On nettoie le nom dans le cas d'une classe anonyme d�finie dans une
// m�thode (method) car McCabe remonte ce cas de cette fa�on :
// Pour une classe : package.ClassName.method(args)$_Anon_001_
// Pour une m�thode : package.ClassName.method(args)$_Anon_001_.subMethod(args)
// Or on veut une coh�rence avec le compilateur java qui n'indique pas la m�thode
// dans laquelle la classe est d�finie.
newName = newName.replaceAll( "\\.[^\\.]+\\(.*\\)\\$", "\\$" );
return newName;
}
/**
* Supprime les chiffres en d�but de nom.
*
* @param pName le nom � nettoyer
* @return le nom sans chiffre en d�but.
*/
private String clearName( String pName )
{
String badBeginning = "^[0-9]+";
return pName.replaceFirst( badBeginning, "" );
}
/**
* D�compose le nom enti�rement qualifi� d'une m�thode afin de r�cup�rer le nom absolu de la classe.
*
* @param pAbsoluteMethodName le nom absolu de la m�thode
* @return le nom absolu de la classe
*/
private String getAbsoluteClassName( String pAbsoluteMethodName )
{
// s'il y a une parenthese (m�thode) il ne faut pas chercher dans les arguments
int par = pAbsoluteMethodName.lastIndexOf( "(" );
if ( par == -1 )
{
par = pAbsoluteMethodName.length();
}
int index = Math.max( pAbsoluteMethodName.lastIndexOf( ".", par ), pAbsoluteMethodName.indexOf( "#" ) );
return pAbsoluteMethodName.substring( 0, index );
}
/**
* @param pAbsoluteClassName le nom absolu de la classe
* @return le package racine de la classe, null si la classe n'a pas de package
*/
public String getFirstPackage( String pAbsoluteClassName )
{
String first = null;
int firstDot = pAbsoluteClassName.indexOf( "." );
if ( 0 > firstDot )
{
first = pAbsoluteClassName.substring( 0, firstDot );
}
return first;
}
/* =========================================================================================================================================== */
/**
* <p>
* This method is used to convert method signature from bytecode back to source code e.g
* <ul>
* <b>from</b>
* <li>loadProperties(Ljava/util/Properties;Ljava/util/List;Ljava/util/Properties;)</li>
* <br />
* <b>to</b>
* <li>loadProperties(Properties,List,Properties)</li>
* </ul>
* </p>
* <p>
* It is particulary usefull as the aggregation of metrics in Squale could only be achieved if the comparison
* between component is efficient.<br />
* Please refer to the comments in the method core for more information.
* </p>
* @param pSignature the signature that has to be converted
* @return a String representation of the signature in the "Squale format"
*
*/
public String getSignatureFromBytecode( String pSignature )
{
/*
* Using Apache BCEL API to get an array of arguments that are not chopped as a generic method is needed. In
* BCEL Utility only java.lang.* classes are chopped.
*/
String[] returnedSignature = Utility.methodSignatureArgumentTypes( pSignature, false );
String finalSignature = "";
/* Iterating over the array or arguments */
for ( int i = 0; i < returnedSignature.length; i++ )
{
/* first testing if the method is a representation of a static class method call */
String singleArgument = chopStaticSignature( returnedSignature[i] );
/* while the last element is not reached */
if ( i < returnedSignature.length - 1 )
{
finalSignature += ClassUtils.getShortClassName( singleArgument ) + ',';
}
else
{
finalSignature += ClassUtils.getShortClassName( singleArgument );
}
}
return finalSignature;
}
/**
* <p>
* In Cobertura signature of method are given in byte-code format. When calling a method from a static class the
* argument contains the long class name and at the end the final "/" is replaced by a "$". In those cases the
* method signature in Squale has to be chopped to the last part of the returned String e.g
* <b>"(Lorg/codehaus/plexus/util/FileUtils$FilterWrapper;)V"</b> to <b>FilterWrapper</b>
* </p>
* @param pSignature the method's signature that has to be chopped
* @return a String representation of the short class name
*/
public String chopStaticSignature( String pSignature )
{
String returnedSignature = pSignature;
/* checking if the signature contains a $ */
if ( StringUtils.contains(returnedSignature, "$" ) )
{
/* Substringing after the last "$" */
returnedSignature = StringUtils.substringAfterLast( pSignature, "$" );
}
return returnedSignature;
}
/**
* <p>
* This method is used to decode constructor name in results containing bytecode so as to aggregate the metrics.
* </p>
* <p>
* As constructors, at the JVM level, are regular methods that are identified by the well-known name "'<'init'>'",
* one must check if the audited method matches the pattern <b><init></b>.
* </p>
* @param pJvmMethodName the name of the method in a byte-code format
* @param pClassName the classname of the tested methos comes from
* @return a String representation of the constructor
*/
public String getConstructorFromByte( String pJvmMethodName, String pClassName )
{
String constructorName = "";
/* if the method extracted from the result file contains '<init>' */
if ( StringUtils.contains( pJvmMethodName, "<init>" ) )
{
/* replace it as it is a constructor */
constructorName = ClassUtils.getShortClassName( pClassName );
}
else
{
/* else it is identical to the source code method name */
constructorName = pJvmMethodName;
}
return constructorName;
}
}