15 aprilie 2020

Obtinerea resurselor de test in Java

Presupunem un director cu calea: src/test/resources/fileprocessor/source

1. ClassLoader:

final String SOURCE_PATH = "fileprocessor/source";
sourceFolder = new File(this.getClass().getClassLoader().getResource(SOURCE_PATH).getFile());


2. Obtinerea caii relativ la proiect:

private static final String SOURCE_PATH = "./target/test-classes/fileprocessor/source";
sourceFolder = new File(SOURCE_PATH);
P.S. Nu folosim pe Bamboo file.getAbsolutePath() !!!

11 aprilie 2020

Angular - ziua 10 - Componente dinamice

Componente dinamice - sunt create la runtime, momentul cand vor trebui sa fie afisate este controlat programatic, cu *ngIf sau cu Dynamic Component Loader

Dynamic Component Loader: componenta este adaugata la DOM prin cod (imperativ) (vs. declarativ, printr-un selector, la *ngIf)
Totusi, varianta prin *ngIf este mai usoara si cea recomandata.

Exemplu cu *ngIf pentru afisarea unei ferestre de eroare:

1. se creeaza o componenta noua, numita de exemplu AlertComponent, cu satelitii aferenti, si se importa in app.module.ts langa celelalte componente.
@Component({
  selector: 'app-alert',
  templateUrl: './alert.component.html',
  styleUrls: ['./alert.component.css']
})

2. In componenta se adauga:
@Input() message: string; // string settable from outside
@Output() closeEvent = new EventEmitter<void>(); // emits on onClose() for the outside

3. In Html, fereastra de alerta are un buton cu textul "Close", se adauga: (click)="onClose()" pentru a declansa emitter-ul din Typescript.

4. Componenta de alert este folosita in componenta din care vrem s-o chemam, cu *ngIf, trimitandu-i drept input [message] o variabila, iar pe output-ul (closeEvent) devenit input in acea componenta, se merge pe o metoda onHandleError din componenta curenta:
<app-alert [message]="error" *ngIf="error" (closeEvent)="onHandleError()"></app-alert>

5. In onHandleError() avem grija sa schimbam conditiile pentru care *ngIf este true, deci putem seta this.error = null acolo.

----

Varianta programatica ( mai grea, pentru acelasi scenariu )

1. Se creeaza o componenta noua, adnotata cu @Directive, care va reprezenta definitia unui obiect in DOM (de fapt gata sa fie introdus in DOM). ViewContainerRef reprezinta "un container unde se pot adauga una sau mai multe view-uri la componenta"

@Directive({
  selector: '[appPlaceholder]'})
export class PlaceholderDirective {
  constructor(public viewContainerRef: ViewContainerRef) {
  }
}

2. In Html-ul componentei unde vrem sa atasam componenta dinamica, declaram (este doar o declaratie, nu apare in DOM):
<ng-template appPlaceholder></ng-template> 

3. In TypeScript-ul componentei de mai sus vom realiza adaugarea programatica. Dar intai declaram variabilele clasei:
@ViewChild(PlaceholderDirective) alertHost: PlaceholderDirective;
private closeSub: Subscription;

4. Se injecteaza in constructor: private componentFactoryResolver: ComponentFactoryResolver

Intr-o metoda de showErrorAlert():
5. Se obtine din alertHost proprietatea de tipul ViewContainerRef declarata, se face clear() pe ea

6. Din "fabrica" de componente se obtine sub-fabrica de tipul AlertComponent:
const alertCompFactory = this.componentFactoryResolver.resolveComponentFactory(AlertComponent);

7. Se creeaza componenta dinamica cu ajutorul ei si se populeaza campul message cu ceva ce ar trebui sa primim ca parametru in metoda:
const componentRef = hostViewContainerRef.createComponent(alertCompFactory);
componentRef.instance.message = errorResponse;

8. Se face subscribe pe pe proprietatea closeEvent a componentei, care se salveaza intr-o subscriptie:
this.closeSub = componentRef.instance.closeEvent.subscribe(() => {
  this.closeSub.unsubscribe();
  hostViewContainerRef.clear();
});

Asadar, cand se apasa pe "Close", subscriptia in curs se termina, iar componenta se "curata" astfel disparand de pe ecran.

9. Metoda de mai sus este chemata pe ramura de tratare a erorii dupa login/logout.

08 aprilie 2020

Angular - ziua 9 - Autentificare

Autentificare si protejarea rutelor

- RESTful API is stateless => nu se poate folosi sesiunea, ci: clientul va trimite datele de identificare, iar serverul raspunde cu un token. Clientul salveaza acel token si il foloseste in cereri ulterioare catre server.

- Pregatire server Firebase:
{
  "rules": {
    ".read": 'auth != null',
    ".write": 'auth != null'
  }
}
Authentication -> Set up sign-in method -> Email/password -> Enable

Lucrul cu cereri de autentificare in Firebase:
- se copiaza url-ul generic pentru sign up de la Firebase (la momentul de fata este https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=)
- se adauga la el API key-ul aflat in project overview (Web API Key)
- se trimite o cerere de POST catre acel url nou-format, cu datele: {email: your-email, password: your-password, returnSecureToken: true}
- cererea de POST trebuie sa fie de tipul: post<AuthResponseData>, unde tipul raspunsului este definit de Firebase si este tradus de noi intr-o interfata Typescript astfel:
interface AuthResponseData {
  kind: string;
  idToken: string;
  email: string;
  refreshToken: string;
  expiresIn: string;
  localId: string;
  registered?: boolean; // optional: only for the sign in request
}
- se procedeaza asemanator si pentru log in - folosind celalalt URL specific (https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=) si aceeasi cheie


Variabile cu underscore

export class User {
  constructor(public email: string, public id: string,
              private _token: string, private _tokenExpirationDate: Date) {
  }

  get token() {
    if (!this._tokenExpirationDate || new Date() > this._tokenExpirationDate) {
      return null;
    }
    return this._token;
  }
}

Ce inseamna:
- se poate accesa pe variabila user.token pentru a executa metoda de get
- user.token nu se poate asigna
- metoda get token (getter) defineste pasii pentru obtinerea token-ul


Construirea unui utilizator (Sign up):

return this.httpClient
  .post<AuthResponseData>(url, this.getAuthData(email, password))
  .pipe(catchError(this.handleError), tap(resData => {
          // resData contine expiresIn, adica nr de secunde pt care e valabil token-ul
         const expirationDate = new Date(new Date().getTime() + +resData.expiresIn * 1000);
        // resData mai contine si id-ul local in Firebase, cat si token-ul
        const user = new User(resData.email, resData.localId, resData.idToken, expirationDate);
        this.user.next(user);
}));

// unde user: Subject<User>;

- se procedeaza si la login la fel ca la signup.


Operatori rxjs (lista completa aici)

* pipe - un fel de stream() din Java 8, poate primi ca argument alti operatori rxjs, se poate chema pe un Observable, ca rezultat aduce fluxul de valori

* tap - un fel de forEach din Java, se foloseste cu pipe ...  ex: clicks.pipe(tap(ev => console.log(ev))); Poate avea efecte laterale, daca schimbi ceva in ev.

* map - asemanator cu map din Java, primeste o singura valoare din sir si poate face ceva cu ea, fara efecte laterale ... ex: from([1, 2, 3]).pipe(map(item => item + 2)).subscribe(item => console.log(item));

* take - primeste ca argument un numar N, emite doar primele N valori emise de Observabil, nu asculta la infinit ...  ex: interval(1000).pipe(take(3)).subscribe(x => console.log(x)); // 0, 1, 2

* exhaustMap - asteapta primul Observable sa termine, apoi ia urmatoarea valoare (???)
ex: const exhaustSub = firstInterval.pipe( 
        exhaustMap(f => { console.log(''); return secondInterval; }) 
      );


return this.authService.user.pipe(take(1), exhaustMap(user => {
  const urlWithToken = this.URL + '?auth=' + user.token;
  return this.httpClient.get<Recipe[]>(urlWithToken);
})

BehaviorSubject - un Subject care da acces si la valoarea anterioara emisa (cu next), chiar daca nu a fost facut subscribe la acel moment (este folosit cu exhaustMap)
Exemplu: user = new BehaviorSubject<User>(null); // null e prima valoare initiala


Interceptori

- se poate folosi pentru a atasa fiecarei cereri token-ul de care are nevoie

- se creeaza ca serviciu:

@Injectable()
export class AuthInterceptorService implements HttpInterceptor {

  constructor(private authService: AuthService) {
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return this.authService.user.pipe(take(1),
      exhaustMap(user => {
        if (!user) {
          return next.handle(req); // unchanged
        }
        // adds token to all outgoing requests
        const modifiedReq = req.clone({params: new HttpParams().set('auth', user.token)});
        return next.handle(modifiedReq);
      })
    );
  }
}

- se adauga in app.module.ts -> providers, ca: {provide: HTTP_INTERCEPTORS, useClass: AuthInterceptorService, multi: true}

Logoutthis.user.next(null);
De asemenea se curata Local Storage de orice informatie despre user: localStorage.clear();
Alternativ, daca vrem sa stergem doar informatia adaugata explicit despre user: localStorage.removeItem('userData');

Auto-login: prin cookies sau prin Local Storage API furnizat de browser

Pentru varianta Local Storage API:
1. se cheama in Typescript: localStorage.setItem('userData', JSON.stringify(user)); pentru a salva datele despre utilizator la momentul crearii lui
2. intr-o metoda autoLogin() din serviciul de autentificare, se  obtin datele inapoi prin: JSON.parse(localStorage.getItem('userData')); Se reconstituie utilizatorul si se emite BehaviorSubject-ului user (recomandabil daca apeland token() nu se primeste null, deci daca inca are token-ul valid).
3. in app.component.ts se implementeaza OnInit, iar in ngOnInit() se cheama metoda autoLogin() din serviciul de autentificare

Auto-logout: la expirarea token-ului (default dupa o ora in cazul Firebase).

1. se creeaza o metoda in serviciul de autentificare in care, in plus, salvam timer-ul intr-o variabila globala:
autoLogout(duration: number) {
  this.tokenExpirationTimer = setTimeout(() => {
    this.logout();
  }, duration);
}

2. aceasta metoda este chemata de fiecare data cand se face login sau auto-login in serviciu

3. in metoda de logout() avem grija sa distrugem timer-ul:
if (this.tokenExpirationTimer) {
  clearTimeout(this.tokenExpirationTimer);
}
this.tokenExpirationTimer = null;


Protejarea rutelor - unele pagini nu a ar trebui sa fie accesibile utilizatorilor nelogati

1. se creeaza un nou serviciu care implementeaza CanActivate. Metoda canActivate() va returna un boolean, corespunzand permisiunii de a accesa o anumita pagina (true pentru utilizatorii logati).

@Injectable({providedIn: 'root'})
export class AuthGuard implements CanActivate {

  constructor(private authService: AuthService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot)
                    : Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {

      return this.authService.user.pipe(map(user => !!user)); // true if user exists, false otherwise
  }
}

2. in app-routing.module.ts, pe linia componentei care ne intereseaza se adauga: canActivate: [AuthGuard]

3. Pentru redirectare in functie de permisiune, se adauga:

return this.authService.user.pipe(
  map(user => !!user),  // true if user exists, false otherwise  tap(auth => {
    if (!auth) {
      this.router.navigate(['/auth']);
    }
  }));

Alternativ cu UrlTree (un alt output posibil al metodei canActivate()):

return this.authService.user.pipe(
  take(1),
  map(user => {
    const isAuth = !!user;
    if (isAuth) {
      return true;
    }
    return this.router.createUrlTree(['/auth']);
  }));

!!! take(1) s-a folosit pentru a lua doar ultimele valori introduse, pentru ca garda sa nu continue sa asculte la infinit

01 aprilie 2020

Angular - ziua 8 - Cereri HTTP

Cereri HTTP
Angular poate comunica prin intermediul HTTP Requests/Responses catre un Server (API - REST, GraphQL) care la randul sau comunica cu o baza de date.
Documentatia oficiala aici.

Cererea HTTP:
- URL
- HTTP verb: POST, GET, PUT
- Headers (metadata): {"Content-Type" : "application/json"}
- Body: {title : "New Post"}

Solutie de serviciu backend:  Firebase de la Google
- Realtime database (start in test mode)
- URL-ul furnizat poate fi URL-ul la care se pot trimite cereri din Angular

Pasi pentru a folosi HttpClient:
1. app.module.ts -> imports -> HttpClientModule

2. HttpClient se injecteaza in constructorul clasei unde dorim sa-l folosim

3. Trimiterea unei cereri de POST: this.httpClient.post(url: String, postData: object); 
// in functie de url, Firebase va aseza datele intr-o structura de dosare
// postData va fi convertit automat intr-un json

!!! post() intoarce un Observable. Simpla chemare a lui post() nu va declansa nimic, dupa principiul: daca nimeni nu este interesat (= subscription), nu se executa. Asadar:

this.httpClient.post(url: String, postData: object).subscribe(responseData => {
  // do something
});
// nu este nevoie de unsubscribe() pentru ca este un Observable administrat de Angular


GET
this.httpClient.get(url).subscribe(posts => {
  // do something
});


Prelucrarea datelor:

this.httpClient
  .get(url)
  .pipe(map(post => {
    // prelucreaza & return data
  }))
  .subscribe(posts => {
    // do something
  });


Tipizare:

this.httpClient
  .get<{ [key: string]: Post }>(url)
  .pipe(map(post => {
    // process & return data
  }))
  .subscribe(posts => {
    // do something
  });

- unde: [key: string] inseamna orice proprietate cu rol de cheie (nu ii cunoastem numele, este generata la intamplare de Firebase), iar Post este tipul valorii furnizate de aceasta bucata de date
- tipizarea este valabila si pentru alte metode, cum ar fi post()

Se recomanda ca actiunile Http sa se realizeze in servicii.
- fie adnotate cu @Injectable({providedIn: 'root'})
- fie se inscriu in app.module.ts -> providers
- se poate face subscribe concomitent in serviciu (pentru ca actiunea sa se execute), si in componenta care cheama serviciul (doar daca o intereseaza raspunsul)


DELETE
this.httpClient.delete(url, object).subscribe( () => { // empty response
  // here you can set up some flags
});

!!! Pentru separarea intre componenta/serviciu, in serviciu se face delete (fara subscribe), iar in componenta se menajeaza variabilele.


Tratarea erorilor:

error: String;

// ...

this.httpClient.get(url).subscribe(posts => {
  // do something
  this.error = null;
}, error => {
  this.error = error.message;
});

Alt mod de a trata erorile, in serviciu:

error = new Subject<string>();

this.httpClient.post(url, postData).subscribe(responseData => {
  // ..
}, error => {
  this.error.next(error.message);
});

In componenta, in ngOnInit():
this.service.error.subscribe(errorMessage -> {
  this.error = errorMessage;
});

// in acest caz se cuvine sa faci unsubscribe, in ngOnDestroy()


Operatorul catchError:

this.httpClient
  .get(url)
  .pipe(map(post => {
    // prelucreaza & return data
  }), catchError(errorRes => {
    // generic error handling task, like logging / analytics
    throwError(errorRes); // it needs to be able to reach subscribe again
  }))
  .subscribe(posts => {
    // do something
  });


Antete, query params
this.httpClient.get(url, {
  headers: new HttpHeaders({'Custom-Header': 'Hello'}),
  params: new HttpParams().set('print', 'pretty') // echivalent cu url/page.json?print=pretty
}); // pt GET antetul trimis va fi al doilea argument

Alternativ, pentru query params:
let searchParams = new HttpParams(); // obiect imutabil
searchParams = searchParams.append('print', 'pretty');
searchParams = searchParams.append('custom', 'key');
// params: searchParams 

Observare:
this.httpClient.post(url, data, {observe: 'response'}); // response, body, events
this.httpClient
  .delete(url, {observe: 'events'})
  .pipe(tap(event => { // tap nu intrerupe felul cum decurge observabilul, primeste doar events
    if (event.type === HttpEventType.Response) { // verifica daca raspunsul s-a primit
      console.log(event.body);
    }
  })); 

Schimbarea tipului de raspuns:
this.httpClient
  .delete(url, {
    observe: 'events',
    responseType: 'json' // text, blob, etc
  })


Interceptori

export class AuthInterceptorService implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler) {
    // next e o functie
    console.log('Request is on its way');
    return next.handle(req); // let the request leave the app
  }
}

In app.module.ts -> providers -> [{provide: HTTP_INTERCEPTORS, useClass: AuthInterceptorService, multi: true}]

Manipulare Request (care este imutabil):
const modifiedRequest = req.clone({
  headers: req.headers.append('Auth': 'xyz')
});
return next.handle(modifiedRequest);

Interceptori pentru raspuns - in return se manipuleaza raspunsul:
return next.handle(req).pipe(tap(event => {
  if (event.type === HttpEventType.Response) {
    // console.log (event.body);
  }
}));