Outils pour utilisateurs

Outils du site


techniques:testify

testify-jhi est l'un de mes projets java. Il est constitué de java,javascript,angular1.0, liquibase, hibernate,spring, spring-boot Bref, très intéressant. Le cadre de cet article est de fournir des mémos et aide pour travailler avec.

Node.js

jhipster utilise node.js ; plus précisément, il utilise le module yeoman et le générateur (plugin) jhipster. Voir mon article node.js pour la mise en place d'un projet jhipster

Maven

> projet parent : spring-boot La 1ere chose à noter est qu'un projet jhipster hérite d'un parent sprint-boot :

<sxh xml>

 <parent>
    <artifactId>spring-boot-starter-parent</artifactId>
    <groupId>org.springframework.boot</groupId>
    <version>1.4.1.RELEASE</version>
    <relativePath/>
 </parent>

</sxh>

Remarque : on retrouve ce projet dans <Dossier .m2>\repository\org\springframework\boot\spring-boot-parent\1.4.1.RELEASE

> Propriétés des dépendances tirées et plugins : On y retrouve aussi des propriétés (comme les versions) des dépendances et des plugins utilisés, notamment :

  • Hibernate 4.3.11.Final
  • java.version
  • liquibase 3.4.2
  • spring-security 4.1.0
  • profiles no-liquibase
  • swagger : permet de publier toutes les méthodes des services rest publiés par l'appli

Swagger détecte automatiquement et fournit la description de chaque méthode, et permet de les invoquer

  • et même des propriétés sonar

Lancement de l'application

Lancer avec maven

 # mvn org.springframework.boot:spring-boot-maven-plugin:1.3.6.RELEASE:run

ou encore

 # mvn spring-boot:run

Lancer directement via eclipse, en debug

  • Cliquer sur le bouton de Debug
  • Dans projet, tapper le nom du projet eclipse JHIPSTER
  • Dans main class, parcourir pour trouver la classe dont le nom se termine par App ; elle est annoté avec :

<sxh java>

  @ComponentScan
  @EnableAutoConfiguration(exclude = { MetricFilterAutoConfiguration.class, MetricRepositoryAutoConfiguration.class })
  @EnableConfigurationProperties({ JHipsterProperties.class, LiquibaseProperties.class })

</sxh>

  • Cliquer sur debug

Points d'entrées dans l'application web

Points d'entrée général

http://localhost:8080/#/

Il donne accès, via le menu en haut :

  1. à un utilitaire de gestion des entités en base de donnée
  2. à la gestion des comptes utilisateurs
  3. à swagger API, inventaire de tous les services REST exposés
  4. aux logs
  5. aux audits, métriques, santé de l'application

Points d'entrée fonctionnel

Espace candidat

http://localhost:8080/candidat.html#/<idCandidat>/<idInvitation>

Aller chercher en base le bon candidat Saisir le token

Espace recruteur

Points d'entrée directs : services rest

Il est possible d'invoquer directement les services via son navigateur ou via postman, ou encore swagger.

Par exemple : http://localhost:8080/api/bonuses

–> fournira tous les enregistrement bonus présents en base.

Explication du lancement de l'application, beaucoup d'info dans TestifyJhiApp

E1 : Debut de la méthode TestifyJhiApp.main()

 SpringApplication app = new SpringApplication(TestifyJhiApp.class);

–> E1.1 : Positionnement de l'environnement

> dans TestifyJhiApp.main() : On retrouve le code qui construit l'instance Environment. Un environnement contient des propriétés et les profiles actifs (dev, swagger, etc …).

L'environnement est chargé à partir d'un fichier application-<xxx>.yml. Ce chargement est effectué lors de l'appel suivant :

  Environment env = app.run(args).getEnvironment();

On peut donc changer l'environnement en fournissant des arguments de la ligne de commande.

  .--spring.profiles.active=<your-active-profile>

<your-active-profile> peut être :

  • dev
  • prod
  • autreEnvironnement si on a créé un fichier application-autreEnvironnement.yml

On peut analyser le code SpringApplication.configureProfiles() pour mieux comprendre … L'environnement créé sera en fait une instance vide StandardServletEnvironment.

Puis les profils actifs seront configuré par l'appel de SpringApplication.prepareEnvironment()

  .SpringApplication.run()
  .    --> SpringApplication.prepareEnvironment()
  .        --> EventPublishingRunListener.environmentPrepared()
  .           --> initialMulticaster.multicastEvent(new ApplicationStartedEvent( ...
  .          Tout ceci permet de charger les profils spring actifs ; l'environnement n'est pas encore chargé.
  .    --> la bannière est ensuite affiché
  .    --> SpringApplication.createApplicationContext() : définit que la classe de contexte à trouver est une AnnotationConfigEmbeddedWebApplicationContext
  .         --> return BeanUtils.instantiate(contextClass);
  .    --> SpringApplication.postProcessApplicationContext() : ???
  .    --> SpringApplication.load(...)
  .    --> SpringApplication.refreshContext() : c'est dans cette méthode que les propriétés de l'environnement sont chargées.

A un moment, il ajoute dans le champ StandardServletEnvironment.propertySources 2 items:

   MapPropertySource [name='applicationConfig: [classpath:/config/application.yml]']
   MapPropertySource [name='applicationConfig: [classpath:/config/application-dev.yml]'] 

Ce sont bien dans ces 2 objets que les propriétés de l'environnement (fichiers application.yml) sont chargées

> Puis, dès que l'instance Environnement est créée, le scan des beans spring va l'injecter dans l'attribut TestifyJhiApp.env :

  @Inject
  private Environment env;

–> E1.2 instancie SpringApplication en fournissant la classe TestifyJhiApp comme source. Cela implique qu'elle va être chargée comme bean dans l'applicationContext. Mais elle annotée avec :

  • @ComponentScan, qui indique une directive de scan d'autres composants dans le code
  • @EnableAutoConfiguration, qui indique une politique de recherche de composants spring à partir du classpath, en excluant les packages spécifiés
  • @EnableConfigurationProperties({ JHipsterProperties.class, LiquibaseProperties.class })

Par exemple, LiquibaseProperties est annotée avec le prefix “jhipster” =⇒ cela indiquera que la conf de jhipster sera chargée à partir du champ jhipster du fichier spring de propriétés arborescentes application-<xxx>.yml, <xxx> selon l'environnement.

En l'occurrence, si on lance l'environnement de dev, alors c'est application-dev.yml qui sera utilisé. Dedans on retrouve :

 liquibase:
    contexts: dev

donc le champs LiquibaseProperties.contexts sera setté à “dev”.

E2 : PostConstruct init application Quand tous les beans spring sont initialisés, la méthode suivante est appelée :

 TestifyJhiApp.initApplication()

Cette méthode ne fait rien d'autre que de logger les profils chargés, et de logguer si une erreur de config apparait au niveau des profils.

Fichier de conf de lancement de l'application

AsyncSpringLiquibase, DatabaseConfiguration

AsyncSpringLiquibase

extends SpringLiquibase ; son but est de permettre, si les profils actifs contiennent “dev” ou “heroku” (ils sont positionnés dans application-<xxx>.yml), de lancer le process liquibase de manière asynchrone en utilisant un TaskExecutor, et dans le cas contraire, de lancer le process liquibase de manière synchrone, i.e pendant le chargement de l'application.

AsyncSpringLiquibase .afterPropertiesSet() est appelé quand ce bean est entièrement initialisé, i.e que ses dépendances ont été settées (@Inject : taskExecutor, env)

DatabaseConfiguration

C'est cette classe qui se charge d'instancier le bean AsyncSpringLiquibase.

DatabaseConfiguration est annotée avec 3 annotations :

@Configuration

Elle définit le bean SpringLiquibase en settant ses différentes propriétés. Remarque : La définition du bean dépend d'un bean DataSource et d'un bean LiquibaseProperties. La datasource est instanciée à partir de sa définition dans le fichier application-<xxx>.yml (spring:datasource:…)

@EnableTransactionManagement

Annotation équivalente à <tx:annotation-driven/> dans un xml spring. Elle permet de faire en sorte les annotations @Transactional ou dérivées des concepts de Aspect-J ou proxy soient prise en compte sur des méthodes.

@EnableJpaRepositories

(voir plus bas) ; les JpaRepositories se trouvent dans fr.softeam.testify.repository. –> impose de créer un SimpleJpaRepository qui implémente chaque interface JpaRepository trouvée dans le package spécifié du classpath spécifié

Or les SimpleJpaRepository sont annotés avec @Transactional ; leur utilisation est rendue possible grace à l'annotation précédente, @EnableTransactionManagement

LiquibaseProperties : classe annotée de la manière suivante :

 @ConfigurationProperties(prefix = "liquibase", ignoreUnknownFields = false)

Cela implique que spring tentera, à partir de ses fichiers de propriétés de setter les champ de ce bean qui va être créé. On retrouve seulement spring:liquibase:contexts dans le fichier application-dev.yml. On aurait pu tout aussi bien ajouter dans le fichier yml la ligne suivante :

 changelog: classpath:config/liquibase/master.xml

Ca aurait évité de l'avoir en dur dans le code, tel que généré par le générateur jhipster.

Quelques annotations spring

@Configuration : Specifies the class as configuration. Elle permet de remplacer le fichier des beans beans.xml ; Typiquement, on retrouvera dans ce genre de classe des définition de bean, comme par exemple.

<sxh java>

 @Bean
 public String aStringBean() { return "yesmec";};

</sxh>

@EnableTransactionManagement : Enables Spring's annotation-driven transaction management capability. C'est de la programmation aspect, qui permet, à l'aide des annotation @Transactional au début de méthodes invoquant des appels hibernate, de commencer une transaction au début de la fonction, et de la terminer en commit ou rollback à la fin de l'exécution de la fonction

@EnableJpaRepositories(“pfder”) : indique qu'il faut instancier toutes les interfaces JpaRepositories (DAO) avec des SimpleJpaRepositories

@EnableAspectJAutoProxy : Prendra en charge les composants dont les classes sont annotées avec 2 annotations suivantes combinées :

 @Component
 @Aspect

Securite : acces au backoffice

Elle définie dans la classe de conf SecurityConfiguration ;

Cette classe hérite de WebSecurityConfigurerAdapter, qui redéfinit notamment les méthodes suivantes : <sxh java>

  @Override
  public void configure(WebSecurity web) throws Exception {
      web.ignoring()
          .antMatchers(HttpMethod.OPTIONS, "/**")
          .antMatchers("/app/**/*.{js,html}")
          .antMatchers("/bower_components/**")
          .antMatchers("/i18n/**")
          .antMatchers("/content/**")
          .antMatchers("/swagger-ui/index.html")
          .antMatchers("/test/**");
  }

</sxh>

Cette configuration permet l'accès aux URL mentionnées. Heureusement, sinon pas d'appli web.

<sxh java>

  @Override
  protected void configure(HttpSecurity http) throws Exception {
      http
          .csrf()/*.disable().*/
      .and()
          .addFilterAfter(new CsrfCookieGeneratorFilter(), CsrfFilter.class)
          .exceptionHandling()
          .accessDeniedHandler(new CustomAccessDeniedHandler())
          .authenticationEntryPoint(authenticationEntryPoint)
      .and()
          .formLogin()
          .loginProcessingUrl("/api/authentication")
          .successHandler(ajaxAuthenticationSuccessHandler)
          .failureHandler(ajaxAuthenticationFailureHandler)
          .usernameParameter("j_username")
          .passwordParameter("j_password")
          .permitAll()
      .and()
          .logout()
          .logoutUrl("/api/logout")
          .logoutSuccessHandler(ajaxLogoutSuccessHandler)
          .deleteCookies("JSESSIONID", "CSRF-TOKEN")
          .permitAll()
      .and()
          .headers()
          .frameOptions()
          .disable()
      .and()
          .authorizeRequests()
          .antMatchers("/api/fo/candidate/**").permitAll()
          .antMatchers("/api/register").permitAll()
          ...
          .antMatchers("/api/**").authenticated()
          .antMatchers("...")            
          ...

</sxh>

Le dernier bloc est intéressant : .authorizeRequests()

Ca va exclure du controle d'autorisation les url mentionnées qui sont suivies de .permitAll() est invoqué.

Par contre, pour certaines autres, on a authenticated().

C'est justement le cas des pages du recruteur.

Pour y accéder, il faut d'abord être authentifié. Après il faut comprendre comment fonctionne l'authentification.

Securite : authentification

Angular js 1.0

Avant de commencer, quelque concepts à savoir :

Le préfix $ = du framework angular

'$', devant des noms variables sert à indiquer que ce sont des instances créées par le framework angular lui même et non dans une des classes développées. Ce sont des instances propres à angular : on retrouve $scope, $rootscope, $locationProvider, $stateProvider, etc …

Notion de $scope et de $rootScope

Le $rootScope correspond à une hashmap contenant toutes les variables liées à la vue du MVC (comprendre la page web), ou plus précisément toutes les balises html ou angular contenu dans la balise 'root' doté de l'attribut ng-app.

$scope correspond aussi à une hashmap contenant des objets, mais il est lié à des balises contenue dans la balise 'root'.

Ces scopes sont intimement lié au concept de bidirectionnel : on peut changer les valeur dans la vue (champ de saisie par exemple) et elles seront automatique répercutées dans l'objet du scope associé. Inversement, si un code javascript change la valeur d'un objet du scope, alors la récupercussion sur la vue sera automatique. C'est ça une des grandes puissances d'angular.

Scopes provide separation between the model and the view, via a mechanism for watching the model for changes. They also provide event emission/broadcast and subscription facility.

Beaucoup d'asynchrone

Angular impose l'utilisation d'asynchrone ; plus particulièrement, de la même manière que pour ajax, quand on appelle certaines fonctions, on doit fournir 2 handlers, un dans le cas où la fonction de retourne un succès, l'autre dans le cas où elle retourne un échec. L'avantage ajouté est que l'on peut fournir une promesse de résolution de la fonction au code appelant, donc positionner les 2 handlers dans le code appelant :

Dans la méthode appelée : <sxh js>

 function functionExecuteDeManiereAsynchrone(arg1, arg2) {
    var deferred = $q.defer();
     ...
     code asynchrone qui appellera soit : 
        deferred.resolve({des parametres pour le handlers });  
        deferred.reject({des parametres pour le handlers });  
     ...
    return deferred.promise;
 }

</sxh>

Code faisant suite à la résolution de la promesse : <sxh js>

 function handlerSucces(params) {
   ...
 }
 
 function handlerEchec(params) {
   ...
 }

</sxh>

et l'appel à la fonction qui retourne la promesse

<sxh js>

 functionExecuteDeManiereAsynchrone(arg1Val, arg2Val).then(handlerSucces(params), handlerEchec(params) );

</sxh>

Description de la page web candidat.html :

ng-app

ng-app est une directive angular identifiant un module angular. <sxh html>

 <body ng-app="testifyJhiApp" ...

</sxh>

Sa valeur se retrouvera dans la définition du module, dans un fichier js :

<sxh js>

var moduleCandidate = angular.module('testifyJhiApp', [
        'ngStorage', 
        'ngResource',
        'ngCookies',
        'ngAria',
        'ngCacheBuster',
        'ngFileUpload',
        'ui.bootstrap',
        'ui.bootstrap.datetimepicker',
        'ui.router',
        'infinite-scroll',
        // jhipster-needle-angularjs-add-module JHipster will add new module here
        'angular-loading-bar'
    ])
    .run(run);

</sxh>

Ce bloc instancie un module nommé testifyJhiApp, et dépendant d'autres instances de module ; Par exemple : ui-rooter est une instance définie dans le fichier angular-ui-router.js. <sxh js> angular.module('ui.router', ['ui.router.state']); </sxh>

En fait, on retrouve ces modules communs dans le dossier bower-components.

Remarque : l'import des modules communs utilisés devront forcément être placés dans le fichier candidat.html avant le module de notre application testifyJhiApp, sinon ca va planter. <sxh js>

  <script src="bower_components/jquery/dist/jquery.js"></script>
  <script src="bower_components/json3/lib/json3.js"></script>
   ...
  <script src="bower_components/angular-ui-router/release/angular-ui-router.js"></script>
  ...
  
  <script src="notreApplication.js"></script>

</sxh>

Notion d'injection de dépendance : stateHandler

On retrouve ensuite le code suivant :

<sxh js>

 run.$inject = ['stateHandler'];
 function run(stateHandler) { stateHandler.initialize(); }

</sxh>

Cela signifie que run sera défini comme une fonction et qu'elle dépendra du service stateHandler, instancié dans le fichier state.handler.js. Il s'agit d'une 2e forme d'injection de dépendance.

Remarque : on peut injecter n'importe quel type d'instance : des services, des filtres, ou d'autres objets

Partie controlleur du MVC détenu coté angular

<sxh js>

 moduleCandidate.config(stateConfig);

</sxh>

indique qu'il faut lancer la fonction stateConfig au chargement du module moduleCandidate. stateConfig est définie juste après :

<sxh js>

  stateConfig.$inject = ['$locationProvider','$stateProvider'];    
  function stateConfig($locationProvider,$stateProvider) 
  {
      $stateProvider
      .state('uistate_app', {
          url: '/',
          data: { pageTitle: 'Testify : point d\'entrée' },            
          views: {
              'uiv_error@': {
                  templateUrl: 'app/layouts/candidateTmpl/tmpErrors.html',
                  controller: 'Crtl_EntryPointErrors',
                  controllerAs: 'vm'
              }
      		,'uiv_evaluations@': { templateUrl: undefined, controller: undefined }                
              ,'uiv_currentQuestion@': { templateUrl: undefined, controller: undefined }
          }
      	/*, resolve: { authorize: ['Auth',function (Auth) { return Auth.authorize(); } ] } */
      })
      .state('uistate_statusError', { ...

</sxh>

$stateProvider.state() permet de créer un état (un peu comme dans struts) ;

On peut faire naviguer la page web du module d'un état à un autre. Chaque état est associé à un état 'fonctionnel' de présentation de l'application. La partie vue du MVC (page web) un ensemble de vues (balise html dotée de la directive ui-view=“nomDeLaVue”). Ces vues contiennent du code html ou angular. Lorsque l'on passe d'un état à un autre, l'objectif est de faire afficher à ces vues des choses différentes et de les faire se comporter de manière différentes (toujours un peu comme les tuiles de struts)

Pour cela, dans le bloc de code montré ci-dessus, on reparamètre, on définit à l'avance, pour chaque état qui pourra être atteint dans cette application moduleCandidate :

  • une url, qui sera positionné dans la barre d'adresse
  • des données qui pourront être récupérées par la page web pour les afficher ( data: { … } )
  • pour certaines des vues de la page web : controller, templateUrl

controller : ce sont des fonctions qui seront appelées pour permettre de mettre en œuvre le comportement de la page web : les listeners, des handlers sur click, positionne des valeurs qui seront récupérées, des valeurs en session front angular.

  • d'autres éléments dédié à cet état (comme resolve:)

Ici, par exemple on créé un state uistate_app. On pourra, par exemple dans un controlleur, définir un code pour aller de l'état courant à l'état uistate_app via l'appel suivant : <sxh js> $state.go('uistate_listEvaluations', { parametresDeLEtatCiblé }) </sxh>

On peut aussi aller à un état en tapant dans la barre d'adresse une URL qui match une des urls définies dans un état.

Remarque : Dans le cas où on fournit des paramètres lors du changement d'état, ils sont insérés dans $stateParams. Si bien que si l'état ciblé est associé à des controlleurs, ces derniers pourront récupérer les valeurs passées en utilisant l'objet $stateParams.

Point d'entrée de l'application : espace candidat

Il s'agit de l'état uistate_Entry. Il sera accédé quand l'utilisateur saisit dans la barre d'adresse

 http://localhost:8080/testifyjhi/<unIdCandidat>/<unIdInvit>

Dès lors, le contrôleur Ctrl_CandidatEntryPoint est initialisé sur le root scope. Lorsqu'il est appelé, on retrouvera dans $stateParams les valeurs dynamiques saisies dans l'url : :idCandidat et :idInvit Il fait appel à AssertValidEntryPoint(), qui retourne une promesse qui représentera le résultat de l'authentification avec un token de sécurité.

AssertValidEntryPoint

  • Si pas encore défini, définit et lance un timer. Ce timer vérifiera toute les secondes que si l'évaluation a été lancée, alors sa durée n'excède pas la durée prévue. Sinon, elle forcera la fin de l'évaluation et retournera à la liste des évaluations de l'invitation.
  • Créer une promesse *p1*, qu'elle retournera immédiatement :

Créer et lance de manière asynnhcrone une modale de saisie du token de securité associé au candidat et à l'invitation ( Voir createAndOpenDialogSecurityToken() pour comprendre )

On utilise $uibModal.open() pour créer et ouvrir la modale. Cette méthode retourne une instance modale, dont le champ result est aussi une promesse *p2*. C'est la résolution de la promesse p2 (en succès ou en échec) qui entrainera la résolution de la promesse p1.

Dans le cas où l'utilisateur saisie un token et clique sur “Entrer”, alors on passe dans la fonction succès de la promesse p2. Là, on invoquera le service FactoryEntryPointCandidat en lui fournissant les infos d'authentification. Ce service angular invoquera le service backoffice :

 /api/fo/candidate/EntryPoint/:idCandidat/:idInvit?entryPointSecureToken=:entryPointSecureTokenValue

Ce service retournera un json dont result indiquera si l'authentification est valide. Selon la validité de l'authentification, la promesse p1 sera résolu en succès ou échec.

techniques/testify.txt · Dernière modification: 2018/01/31 23:48 (modification externe)