HOAB

History of a bug

SpringBoot OSGI and templated OSGI modules with external constrant Jetty

Rédigé par gorki Aucun commentaire

Problem :

A very specific and particular problem.

I want to deploy  a SpringBoot application, in an OSGI environment, in a external Jetty server…

So I found solution do deploy Springboot in OSGI : here or here. Very good by the wat.

And also to deploy Springboot as war for an external Tomcat…

Too easy, my external Jetty server was rejecting Servlet 3.0 application startup… (hard coding for : 

context.setAttribute("org.eclipse.jetty.containerInitializers", initializers);

Yes, it's better, starting a 1,25 days of try and success !

Solution :

My first web application was deployed with Spring Dispatcher Servlet with a BundleActivator

My second web application was deployed with Spring DispatcherServlet AND a SpringBootApplication intialized with the ressource loader of the BundleActivator (as in the OSGI example) : component-scan is working ! 

My third web application was switching to an initialization Servlet 3.0 with an extension of ServletContainerInitializer, thanks to harded jetty configuration, it's not working

My third and a half remove all web.xml content to keep only a context listener as my Jetty was doing nothing : 

<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">

    <!-- ServletContainerInitializer can not be used here -->
    <!-- XXXXXXX is putting in hard : context.setAttribute("org.eclipse.jetty.containerInitializers with only Jasper -->

    <!-- At the end, it's possible to load SpringBoot with this context listener -->
    <listener>
        <listener-class>
            com.hexagon.introscope.extension.MyContextLoaderListener
        </listener-class>
    </listener>

</web-app>

My fourth web application was trying to load the Springboot application file as a YAML file (why so simple !), resolved with a “hack” :

  • The ServletContext has a way to store the configuration directory, during the initialization, I saved it somewhere : HpaContextLoaderListener.getConfigDirectory() 

The listener loader : 

package com.xxxxx.extension;

import org.eclipse.jetty.webapp.WebAppContext;
import org.springframework.web.context.WebApplicationContext;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletException;
import java.io.File;
import java.nio.file.Path;

public class MyContextLoaderListener implements ServletContextListener {

    private static File configDirectory;

    public static File getConfigDirectory() {
        return configDirectory;
    }

    public void contextInitialized(ServletContextEvent event) {
        ServletContext servletContext = event.getServletContext();
        MySpringBootHexagonActivator app = new MySpringBootHexagonActivator();

        try {
            configDirectory = servletContext.getAttribute("introscope.em"));
            
            InstantiateHelper.getValueByMethod(servletContext, "setExtendedListenerTypes", new Class[]{boolean.class}, true);

            app.onStartup(event.getServletContext());
        } catch (ServletException e) {
            throw new RuntimeException(e);
        }
    }

    public void contextDestroyed(ServletContextEvent event) {
    }
}

 

@SpringBootApplication
public class MySpringBootHexagonActivator extends SpringBootServletInitializer {

    protected SpringApplicationBuilder createSpringApplicationBuilder() {
        SpringApplicationBuilder builder = super.createSpringApplicationBuilder();
        Map<String, Object> properties = new HashMap<>();
        Path propertyFile = Path.of(MyContextLoaderListener.getConfigDirectory().getPath(), "application-extension.yml");
        properties.put("spring.config.location", propertyFile.toString());
        builder.properties(properties);
        builder.resourceLoader(HpaHexagonActivator.getResourceResolver());

        return builder;
    }
}

The HpaHexagonActivator.getResourceResolver() is the trick from SpringBoot OSGI example to load classes from Bundle classpath. I store the ressources resolver in a static variable that I can access from everywhere.

A word on that one : 

InstantiateHelper.getValueByMethod(servletContext, "setExtendedListenerTypes", new Class[]{boolean.class}, true);

I'm in OSGI, Jetty controls the listener class, and Springboot is unknown. You can authorized additional listeners but in my case, Jetty is not exported by the other OSGI package, not able to call it simply so… reflection : InstantiateHelper is basically a helper class to call methods by reflection.

 

At the end : 

  1. Springboot starts
  2. Springboot scans package offered by BundleActivator ressource locator
  3. Springboot can use an external configuration file, in YAML
  4. Springboot starts REST & Database
  5. It was hard.

Of course, lot of tries between these big steps !

The goal is to extend a 3rd party product without having source code.

Springboot OAUTH & PKCS failed & log level of the filter

Rédigé par gorki Aucun commentaire

Problem :

When using Spring Oauth2 resource server, it checks the received token, to do so it must retrieve token validaty or get certificates to validate token. These two calls are usually made in HTTPS for obvious security reason.

When the certificate is not known by the Spring Oauth2 resource server JVM, it fails as SSL handshake can not complete. It fails. Without a single message :) 

Solution :

Put the following log level on : 

org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter: DEBUG

I see that the exception is propagate to authenticationFailureHandler I see that it should be handle by authenticationFailureHandler but somewhere in the chain, it's not traced… Could search deeper next time.

It will activate this log : 

            try {
                AuthenticationManager authenticationManager = this.authenticationManagerResolver.resolve(request);
                Authentication authenticationResult = authenticationManager.authenticate(authenticationRequest);
                ...
            } catch (AuthenticationException var11) {
                SecurityContextHolder.clearContext();
                if (debug) {// 134
                    this.logger.debug("Authentication request for failed!", var11);
                }
                this.authenticationFailureHandler.onAuthenticationFailure(request, response, var11);
            }

Spring data and MongoDB aggregation (GroupBy)

Rédigé par gorki Aucun commentaire

Problem :

I try to create an aggregation (count and groupBy) on my documents.

Thanks to these links :

I nearly resolve my problem... nearly :)

Solution :

Documents to analyze :

  1. the _id of the application is defined by an id attribute...
  2. instance is not present is all documents
{
   phase : "Production",
   application {
      _id : 1234
   },
   instance: "myInstance"
}

The first try is to use springdata easy way : there is not groupBy ! but count is working if you need :

# If instance is set to null, returns the document which do not have the field
countByApplicationIdAndPhaseAndInstanceId(Long applicationId, Phase phase, String instance)

# Another way to test instance presence
countByApplicationIdAndPhaseAndInstanceIdExists(Long applicationId, Phase phase, Boolean exists)

So, thanks to baeldung (many thanks) ! Here is a working solution, take care about :

  1. filtering order ! the Match operation works either on the document, either on the result of the group by ! (see Baeldung example for testing result of the group by)
#Filter on document
        Aggregation aggregation = newAggregation(filterStates, agg);

#Filter on result of the aggregation
        Aggregation aggregation = newAggregation(agg, filterStates);
  1. my applicationId as used in Springdata easy way must be written with Mongo ID : application._id
  2. and application_id could not be present in the group clause as it is filtered (but as it takes me 2 long hours to make it works....)
  3. Don't forget @Id on the result bean. I see somewhere that we can execute the generic command in Mongo and do not take care about the result bean.
  4. The @Id must be on the first item of the group
  5. Do not miss the collection name in the aggregation command (here MONGO_EVALUATION_COLLECTION_NAME)
@Service
public class EvaluationAdditionalRepository {

    private static final Logger LOGGER = LoggerFactory.getLogger(EvaluationAdditionalRepository.class);

    @Autowired
    private MongoTemplate mongoTemplate;


    public List<InstanceCount> getInstanceByApplicationAndPhase(Long applicationId) {

        GroupOperation agg = group("application._id", "instanceId", "phase").count().as("countInstance");
        MatchOperation filterStates = match(new Criteria("application._id").is(applicationId));

        Aggregation aggregation = newAggregation(filterStates, agg);
        AggregationResults<InstanceCount> result = mongoTemplate.aggregate(aggregation, MONGO_EVALUATION_COLLECTION_NAME, InstanceCount.class);

        return result.getMappedResults();
    }
}

And the result bean :

import org.springframework.data.annotation.Id;

public class InstanceCount {

    @Id
    private Long applicationId;

    // Enum are authorized
    private Phase phase;

    private long countInstance;

    private String instanceId;

    public Phase getPhase() {
        return phase;
    }

    public void setPhase(Phase phase) {
        this.phase = phase;
    }

    public long getCountInstance() {
        return countInstance;
    }

    public void setCountInstance(long countInstance) {
        this.countInstance = countInstance;
    }

    public Long getApplicationId() {
        return applicationId;
    }

    public void setApplicationId(Long applicationId) {
        this.applicationId = applicationId;
    }

    public String getInstanceId() {
        return instanceId;
    }

    public void setInstanceId(String instanceId) {
        this.instanceId = instanceId;
    }

    @Override
    public String toString() {
        return "InstanceCount{" +
            ", applicationId='" + applicationId + '\'' +
            ", phase='" + phase + '\'' +
            ", instanceId='" + instanceId + '\'' +
            ", countInstance=" + countInstance +
            '}';
    }
}

 

Yourkit et Tomcat5 sous Windows

Rédigé par gorki Aucun commentaire

Le problème :

Connecter Yourkit à un Tomcat installé localement sur une machine Windows

Tomcat exécuté en tant que service avec le profil Administrateur

Solution :

Bon rappel de ce qui est décrit ici

  • Tomcat5 :  installation / désintallation du service
  • Tomcat5w : fenetre de contrôle du service et de ses paramètres

Il faut donc exécuter ces programmes dans une console en ligne de commande "admin" :

# Démarrer
# Exécuter... 
cmd
# Valider avec : "Ctrl + Shift + Enter"

Sinon, on peut toujours faire bouton droit sur Tomcat5w et "exécuter en tant qu'Administrateur".

Ensuite, ajouter le lien vers l'agent Yourkit :

  • Il faut mettre les chemins courts de Windows !
  • De cette manière :
# Nom court sous windows : 
dir /x c:\
# Pour obtenir au final le chemin complet : 
dir C:\PROGRA~2\YOURKI~1.6\bin\win32\yjpagent.dll

Ajouter le chemin aux paramètres de la JVM du service Tomcat :ourkit

-agentpath:C:\PROGRA~2\YOURKI~1.6\bin\win32\yjpagent.dll

Et ne pas oublier de démarrer Yourkit en mode Administrateur.

Retour d'expérience SAHI

Rédigé par gorki Aucun commentaire

Le problème :

Sahi, ça marche ou ça marche pas ?

Solution :

Bah euh.... difficile à dire.

Sahi c'est un peu comme Selenium, avec évidemment des différences listées ici.

L'utilisation : faire des tests de non-régression sur une IHM GWT.

La cible : Chrome sous Windows

 

Les plus (+) :

- le studio d'enregistrement marche bien

- il est possible d'ajouter des fonctions custom dans les scénarios.

- il est facile de l'intégrer avec Jenkins (en générant des rapports spéciaux)

 

Les moins (-) :

** sur les architectures testées, ça marche plus ou moins bien :

Jenkins + Chrome sous Linux en local :

(sans interface graphique, i.e. sans server X, i.e. headless, i.e avec XVFB) : ça marche mal : problème de stabilité / reproductibilité. Penser à exporter le server X sans XVFB pour débugger les problèmes Chrome Linux.

Jenkins+ Chrome sous Windows à distance :

pas parfait, mais mieux. Les problèmes ne viennent pas forcément de Sahi, mais des VM

Dans les deux cas : Chrome relancé entre chaque test (résultats pas constants) vs. Chrome utilisé en single session (mieux)

 

** la configuration de base marche mal :

  1. Si Chrome est arrêté entre chaque test, il est killé (cf os.properties) => le profile Chrome est alors corrompu
    • il vaut mieux remplacer dans le browser.xml le chemin vers Chrome par un shell à vous pour dézipper un profile clean avant de relancer.
    • du coup préparer votre profile et dézipper le avant chaque lancement de navigateur
  2. Pour bien être intégrer à Jenkins, mettre sous le gestionnaire de sources (dans des répertoires différents, ils n'évoluent pas au même rythme, pas la peine de vérifier les binaires sahi à chaque déploiement) :
    • userdata,
    • binaires sahi,
    • scripts
  3. Utiliser dans ces scripts les variables pour bien maitriser l'environnement :
    • $WORKSPACE de Jenkins
    • $SAHI_HOME
    • ...
  4. Les scripts de base dans userdata ne sont pas très robustes :
    • Pas de kill de Chrome / Arrêt de Sahi si on kill le build Jenkins
    • Il faut bien nettoyer les logs avant de démarrer SAHI
    • Pas d'attente de démarrage de SAHI
    • La sélection des process à killer est plus ou moins hasardeuse à mon avis, on est resté en singleThread

Et ce n'est qu'une vue rapide.

Conclusion :

Bref, des bonnes idées, mais ce n'est pas du "sur étagère", il y a du boulot pour qu'il soit intégré et robuste, sans parler de la rejouabilité des tests que je ne trouve pas exceptionnelle.

Mais bon quelle idée de contrôler un soft qui n'est pas fait pour être contrôlé (les navigateurs ne sont pas pensé pour ça !!)...

Entre Sahi et Selenium ? Celui que vous connaissez le mieux.

 

Quelques scripts :

Exemple de script de démarrage et attente du lancement de SAHI :

#!/bin/bash

./check.sh
if [ $? -eq 1 ] ; then
        echo "Environnement incorrect"
        exit 1
fi


echo --------
echo SAHI_HOME: $SAHI_HOME
echo SAHI_USERDATA_DIR: $SAHI_USERDATA_DIR_TMP
echo SAHI_EXT_CLASS_PATH: $SAHI_EXT_CLASS_PATH
echo --------

#rm -rf $SAHI_USERDATA_DIR/temp/*
#rm -rf $SAHI_USERDATA_DIR/logs/*
#rm -rf $SAHI_USERDATA_DIR/database/*

OUTPUT_LOG=$SAHI_USERDATA_DIR/logs/sahi_output.log

export POI_JARS=$SAHI_HOME/extlib/poi/excelpoi.jar:$SAHI_HOME/extlib/poi/poi-3.7-20101029.jar:$SAHI_HOME/extlib/poi/dom4j-1.6.1.jar:$SAHI_HOME/extlib/poi/poi-ooxml-3.7-20101029.jar:$SAHI_HOME/extlib/poi/poi-ooxml-schemas-3.7-20101029.jar:$SAHI_HOME/extlib/poi/xmlbeans-2.3.0.jar
SAHI_CLASS_PATH=$SAHI_HOME/lib/sahi.jar:$SAHI_HOME/extlib/rhino/js.jar:$SAHI_HOME/extlib/apc/commons-codec-1.3.jar:$SAHI_HOME/extlib/db/h2.jar:$SAHI_HOME/extlib/license/truelicense.jar:$SAHI_HOME/extlib/license/truexml.jar:$POI_JARS

cd $SAHI_HOME
java -Xmx512m -Djava.io.tmpdir=$SAHI_USERDATA_DIR/temp -classpath $SAHI_EXT_CLASS_PATH:$SAHI_CLASS_PATH net.sf.sahi.Proxy "$SAHI_HOME" "$SAHI_USERDATA_DIR" &> $OUTPUT_LOG &
echo $! > $SAHI_USERDATA_DIR/temp/sahi.pid

echo "Attente du demarrage de sahi"

i=0
STATUS_CODE=1
TIMEOUT=20
while [ $STATUS_CODE -eq 1 ] && [ $i -lt $TIMEOUT ]; do
        cat $OUTPUT_LOG | grep "Finished preparing report" > /dev/null
        STATUS_CODE=$?
        echo -n "."
        sleep 1
        ((i++))
done
echo "Serveur sahi OK"

Exemple de script de lancement de tests et kill de Chrome en fin de script (le userdata.properties est créé à la volé pour les paramètres tel que le port du proxy) :

#!/bin/bash

./check.sh
if [ $? -eq 1 ] ; then
        echo "Environnement incorrect"
        exit 1
fi
URL=$1
BUILD_ID=$2
DEBUG=$3

if [ ! $URL ] || [ ! $BUILD_ID ]
then
        echo "usage : start_and_run.sh <URL> <BUILD ID>"
        exit 1
fi

# Generation du fichier de configuration user
echo "############################################" > $SAHI_USERDATA_DIR/config/userdata.properties
echo "# Fichier généré automatiquement, ne pas modifier (cf start_and_run.sh" >> $SAHI_USERDATA_DIR/config/userdata.properties
echo "############################################" >> $SAHI_USERDATA_DIR/config/userdata.properties
cat $SAHI_USERDATA_DIR/config/userdata.properties.template  >> $SAHI_USERDATA_DIR/config/userdata.properties
printf "\n" >> $SAHI_USERDATA_DIR/config/userdata.properties
echo "proxy.port=$SAHI_PROXY_PORT" >> $SAHI_USERDATA_DIR/config/userdata.properties

# There is only one bypass list for both secure and insecure.
$SAHI_USERDATA_DIR/bin/start_sahi.sh

trap 'cat $SAHI_USERDATA_DIR/temp/sahi.pid | xargs kill' SIGINT SIGTERM EXIT

echo "Attente lancement de SAHI"
sleep 2

if [ "$DEBUG" = "DEBUG" ] ; then
        $SAHI_USERDATA_DIR/bin/testrunner.sh tests.suite $URL chromedebug $BUILD_ID
else
        $SAHI_USERDATA_DIR/bin/testrunner.sh tests.suite $URL chrome $BUILD_ID
fi

 

Fil RSS des articles de ce mot clé