综合技术

Angular Tutorial: Angular 7 and the RESTEasy Framework

微信扫一扫,分享到朋友圈

Angular Tutorial: Angular 7 and the RESTEasy Framework
0

Developers who want to quickly build web applications with flexible front-end and backend frameworks can work with multiple tech stacks like MEAN, LAMP, and so on.

One such web application development stack is Angular 7 and RESTEasy. The advantage of having Angular 7 as a framework is that it can be used to develop apps for any kind of device, faster using pre-built templates. Likewise, RESTEasy can be used as your backend framework to provide the REST API services which are light weight and portable to any kind of container.

In this article, we’ll show you how to build a product catalog which displays a list of products that the user can search through, edit, delete, or create products in a page.

Angular 7 will be used on the front-end for the product catalog and RESTEasy will be used for the backend application. We’ll also show how to build both the front- and backend, then integrate the two different framework seamlessly to work as complete web application.

Front-End Web Development

Angular 7 is a framework which can be used to build applications for web, desktop, and mobile devices. As of now, LTS will have support for Angular 7 up to April 18, 2020. It is licensed under the MIT license.

TypeScript is the language used for programming the behaviors, services, etc.

Angular 7 Setup

Node and npm has to be installed. Then the angular-cli command prompt will be installed.

npm install -g @angular/cli

To learn more of the detailed steps, please refer to the Angular site . Once installed, it can be verified by the command  ng -version .

Generate a Basic App Template

An Angular 7 basic template app can be created with the below command:

ng new angular-tour-of-heroes

When we enter the command it asks for routing — say no. We’ll be using CSS for styling, so just enter and then the app will be created. Now go into tge application folder and check tgat the application works by giving the ng serve -o  command to show the application. Use the below command to generate a product component.

ng generate component heroes

Now let’s modify the application to show product catalog. Change the title inside the src/index.html file to something like "Product Inventory Management."

In the app.componen.ts file, change the title to "Product Catalog."

In the generated component file (files are app*, product/*), change all the instances of "heroes" to "products."

The Angular application first starts with the main.ts file, where the Boostrap module is provided, which is the app module. In turn, the app module invokes the app component. That app component renders the view with the following HTML. The title value provided by the app component will hold the string literal which gets display here.

It has been marked with the app-products   directive, which will be a special directive decorated as a component, i.e. a product component.

<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
  <h1>
    {{ title }}!
  </h1>
  <app-products></app-products>
</div>

Display the Product Catalog

Now let’s create the product cards. Inside the product folder, code up the product card as an HTML template. Now the div with the catalog is the outer layout, then the outer frame with div card block is provided. If the user has not selected any products, then the selectedProduct   variable will not be present, so the  card-body   has been applied with all the details and a whole block is iterable for each product in the product list. The reference of the product is passed for all the events.

All the events will be binded with (event)="functionname" . These function are defined in the component.

<div class="catalog">
	<div class="card cardsize clickable"  *ngFor="let product of products" (click)="onSelect(product)">
  		....
  		 <div *ngIf="selectedProduct!=product" class="card-body">
			<h2 class="card-title">{{product.name | titlecase}}<button class="delete" (click)="onDelete(product)">X</button></h2>
			<p class="card-subtitle">Product Details</p>
			<p class="card-text"><span class="propertyname">Id:</span>{{product.id}}</p>
			<p class="card-text"><span class="propertyname">Qty:</span>{{product.qty}}</p>
			<p class="card-text"><span class="propertyname">Brand:</span>{{product.brand}}</p>
			<p class="card-text"><span class="propertyname">Desc:</span>{{product.desc}}</p>
		</div>
	</div>
</div>

The component which is backed by the HTML template for model data binding has to be coded like below. The component is a logical view of the whole web page which consits of an HTML template with TypeScript modules (to learn more about the components that are available, go to the Angular site ).

import { Component, OnInit } from '@angular/core';
import { Product } from '../product';
import { PRODUCTS } from '../mock-products';
import { ProductService } from '../product.service';
import { Logger } from '../logger.service';


@Component({
  selector: 'app-products',
  templateUrl: './products.component.html',
  styleUrls: ['./products.component.css']
})
export class ProductsComponent implements OnInit {

  products = PRODUCTS;//takes the mock products and display the products for view.

  selectedProduct: Product;//On click of the card, it takes that product to selectedProduct variable.

  constructor(private productService: ProductService, private logger: Logger) {
    this.selectedProduct = null;
  }

The product component has been injected with the product service and logger service. The product component class has the member products that have been iterated in the HTML template ngFor.

export class Product {
	id: number;
	name: string;
	qty: number;
	brand: string;
	desc: string;
}

It is assigned to PRODUCTS , which is an initialized global mock constant.

import {Product} from './product';

export var PRODUCTS: Product[] = [
	{id: 11, name: 'Inspiron Laptop', qty: 3, brand: 'Dell', desc: 'It is a 14 inch wide screen laptop. Windows OS with Interl Processor 5'},
	{id: 12, name: 'Power X ', qty: 5, brand: 'LG', desc: 'It has 2300mAH battery and free mobile cover case'},
	{id: 13, name: 'Keyboard', qty: 7, brand: 'TVS Gold Keyboard', desc: 'Its of standard size. It has provision for USB connector'}
];

Upon initializing the component, it invokes the getProducts method of the component, which invokes the product service. This in turn invokes the RESTEasy API to get the information.

ngOnInit() {
    this.getProducts();
  }

  getProducts(): void {
    this.productService.getProducts().
      subscribe((response: any) => {
                                  this.products = [];
                                  response.map((productelem)=>{
                                    this.products.push(productelem.product);
                                  });
                                  console.log(this.products);
                                });
  }

The product service which will invoke the REST API with a basic authorization header will get the observable and return that to the component. The ‘on subscribe’ component gets the response, maps the product object, and pushes this to the products model of the component.

import { Injectable } from '@angular/core';
import { Product } from './product';
import { PRODUCTS } from './mock-products';
import { Logger } from './logger.service';
import { HttpClient } from '@angular/common/http';
import { HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';

const HTTPOPTIONS = {
      headers: new HttpHeaders({
        'Authorization': 'Basic YWRtaW46YWRtaW4=',
      })
    };

@Injectable({
    providedIn: 'root',
})
export class ProductService {

    constructor(private logger: Logger, private httpclient: HttpClient) { }
    getProducts(): Observable<any> {
        this.logger.log("message is getting logged successfully");
        return this.httpclient.get("http://localhost:8080/resteasyexamples/rest/products", HTTPOPTIONS);
    }

We will explain what we do to produce the output from RESTEasy examples later.

Selecting a Card

If the card has been clicked, it opens up for edit, by setting the selectedProduct variable (which is the piece of code in dotted spaces). It opens up the form for all the fields.

<div *ngIf="selectedProduct==product">
  <h2 class="card-title">Create/Edit Product<button class="delete" (click)="onDelete(product)">X</button></h2>
  <form  (ngSubmit)="onSubmit(product)">
    <label>Name:</label><input [(ngModel)]="selectedProduct.name" name="productName" placeholder="product name"/>
    <br/>
    <div style="width: 7rem;">
    <label>Qty:</label><input [(ngModel)]="selectedProduct.qty" name="productQty" style="width:3rem;" type="number" placeholder="product qty"/>
    </div>
<label>Brand:</label><input [(ngModel)]="selectedProduct.brand" name="productBrand" placeholder="product brand"/>
    <br/>
    <label>Desc:</label><input [(ngModel)]="selectedProduct.desc" name="productDesc" placeholder="product desc"/><br/>
    <input style="float:right; width: 5rem;"  type="submit" value="Save" />
  </form>
</div>

The card will be the edit mode if the model variable selctedProduct   holds a reference to the product. This is done by the product component upon selection of the card. It invokes  onSelect , which takes the clicked product card and maps it to the  selectedProduct . Once the user enters the values as required, the submit button triggers the  onSubmit method, which takes the modified product detail values and passes them to the product service.

//when click on product card, the selectedProduct is cached by the class member.
  onSelect(product: Product) : void {
    console.log("product selected");
    this.selectedProduct = product;
  }


  //as the product is double binded, when edit or add product is done, it automatically applies to the products model so unselecting the selectedproduct alone is fine..
  onSubmit(product: Product) {
    console.log(product);
    this.productService.createOrEditProduct(product).subscribe((response:any)=>{
        product.id = response;
    });
    this.selectedProduct = null;
  }

In the product service, it will invoke the REST API by passing the product model and product id (i.e. it invokes the PUT method). Upon successfully updating the product, this code returns the modified id. This modified id will be set back to the product model id field. So, via two way data-binding, those values appears on the screen.

createOrEditProduct(product): Observable<any> {
  if (product.id==0) {
    return this.httpclient.post("http://localhost:8080/resteasyexamples/rest/products", {"product": product}, HTTPOPTIONS);
  } else {
  		return this.httpclient.put("http://localhost:8080/resteasyexamples/rest/products/"+product.id, {"product": product}, HTTPOPTIONS);
	}
}

Add Product

In the UI, there is top div block which contains the Add Product button. This click event will create a new card in the edit mode, i.e. a card will be opened up with the product form so the user can enter the details.

<div>
	<input [(ngModel)]="searchText" placeholder="product name"/>
	<button class="btn btn-primary" type="button" (click)="onSearch(searchText)">Search Product</button>
	<button class="btn btn-primary" type="button" (click)="onAdd(product)">Add Product</button>
</div>

This will invoke the onAdd   method of the component which creates a new instance of the product model and the id will not be set (the id will be assigned only by the backend REST service at the time of submission). The newly created product will also be assigned to the   selectedProduct reference, so upon submit it will be taken care of.

//while clicking on add product, the product entity created with available id and pushed to products array.
  onAdd(product: Product): void {
    console.log("product added");
    product = {id: 0, name: '', qty: 0, brand: '', desc: ''};
    this.selectedProduct = product;
    this.products.push(product);
  }

When the user submits the form in the product card, it goes to the onSubmit   of the product component which internally calls the product REST API’s POST method. The backend REST API will create a product and return the newly assigned product id to the front-end.

Delete Product

In each card, at top-right corner, there is a delete icon to delete the card. When the user clicks it, the onDelete method of the product component will be invoked. It will remove the product from the product list.

//while delete is clicked, the product will be removed from the products modal.
  onDelete(product: Product) : void {
    this.products = this.products.filter(p1 => p1!=product);
    this.productService.deleteProduct(product.id);
  }

This, in turn, invokes the delete method of product service to call the REST API’S DELETE service. This will delete the product and return a successful message.

The product list which is presisted is always cached in the front-end product component in this.products . Now, the search has been done from the UI itself. The user enters the product name partially or completely, then clicking the search button will invoke the following method.

//while searching, the product name search field value given will be searched in each product name field.
  onSearch(searchText: string) : void {
    alert(searchText);
    this.products = this.products.filter(p1 => p1.name.indexOf(searchText)>=0);
  }

Styling is done for the whole Angular 7 application in the styles.css file in the project root path. The styles specific to the product component are manged in the product component folder. Both the file stylings are listed below.

Some key things to be noted in the CSS stylings are that the cards are of the same size and more cards wrap into the next line. This is caused by defining the .catalog   class with  "display:flex; flex-wrap: wrap" . So, irrespective of the content inside the product card, it all comes out to be the same size.

./styles.css

/* You can add global styles to this file, and also import other style files */
/* Application-wide Styles */
h1 {
  color: #369;
  font-family: Arial, Helvetica, sans-serif;
  font-size: 250%;
}
h2, h3 {
  color: #444;
  font-family: Arial, Helvetica, sans-serif;
  font-weight: lighter;
  border-bottom: 1px solid #ddd;
}
body {
  margin: 2em;
}
body, input[type="text"], button {
  color: #888;
  font-family: Cambria, Georgia;
}
.catalog {
  display: flex; 
  flex-wrap: wrap; 
  padding: 5px;
}
.card {
border: 1px solid #ddd !important;
  padding: 0px;
  display: inline-block;
  margin: 5px;
}
.cardsize {
  width: 17rem;
}
.card-title {
  background: #369;
  color: white;
  margin: 0px;
  padding:5px;
  text-align: left;
}
.card-subtitle {
  text-align: left;
  padding-left: 5px;
  font-size: 18px;
  font-weight: bold;
}
.card-text {
  text-align: left;
  padding-left: 5px;
  font-size: 14px;
}
.propertyname {
  font-weight: bold;
}
/* everywhere else */
* {
  font-family: Arial, Helvetica, sans-serif;
}

src/app/products/product.component.css

.clickable {
cursor: pointer;
}
.delete {
float: right;
color: #369;
font-weight: bold;
cursor: pointer;
}
label {
display: inline-block;
padding-bottom: 5px;
width: 3rem;
text-align:right;
}
input {
    width: 13rem;
    display: inline-block;
}

The logger service has also been injected to the component. That logger service gets the message and prints it in the console. This has been decoupled as service, so logging-related logic can be directly applied to the logger service, which impacts the complete application.

import { Injectable } from '@angular/core';

@Injectable({
    providedIn: 'root'
})
export class Logger{
    logs: string[] = [];

    log(message: string) {
        this.logs.push(message);
        console.log(message);
    }
}

Front-end Angular 7 application code sample can be reference from the GitHub link.

Backend RESTEasy API Service

RESTEasy is a simple way to build REST APIs that are portable to any containers. We have used RESTEasy 3.7.0 and has deployed it to Tomcat 8.

Now, create a web application project from the Maven web application template and then add the following dependency for the RESTEasy framework in pom.xml:

<properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <resteasyversion>3.7.0.Final</resteasyversion>
        <tomcat.version>8.5.35</tomcat.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-jaxrs</artifactId>
            <version>${resteasyversion}</version>
        </dependency>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-servlet-initializer</artifactId>
            <version>${resteasyversion}</version>
        </dependency>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-cache-core</artifactId>
            <version>${resteasyversion}</version>
        </dependency>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-multipart-provider</artifactId>
            <version>${resteasyversion}</version>
        </dependency>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-jettison-provider</artifactId>
            <version>${resteasyversion}</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>

The product entity will be created as shown below, which is similar to the front-end.

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Product {
    @XmlElement
    private Integer id;
    private String name;
    private Integer qty;
    private String brand;
    private String desc;
    @XmlTransient
    private Date modifiedDate;

    public Product() {
        this.modifiedDate = new Date();
    }

    public Product(Integer id, String name, Integer availableQty, String brand, String desc) {
        this.id = id;
        this.name = name;
        this.qty = availableQty;
        this.brand = brand;
        this.desc = desc;
        this.modifiedDate = new Date();
    }

    public Product(Integer id, Product product) {
        this.id = id;
        this.name = product.getName();
        this.qty = product.getQty();
        this.brand = product.getBrand();
        this.modifiedDate = new Date();
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getQty() {
        return qty;
    }

    public void setQty(Integer qty) {
        this.qty = qty;
    }

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public Date getModifiedDate() {
        return modifiedDate;
    }

    public void setModifiedDate(Date modifiedDate) {
        this.modifiedDate = modifiedDate;
    }
}

This is a simple product JAX-RS REST API service to create a product, get all products, update a product. and delete a product. All the product lists have been maintained in the in-memory inventory field. It returns the entity and the RESTEasy framework will convert the entity and response and returns the appropriate status code.

@Path("products")
public class ProductService {
    private static Map<Integer, Product> inventory = new HashMap<>();
    static {
        inventory.put(1, new Product(1, "Inspiron Laptop", 5, "Dell", "Laptop will be robust and best suitable for home purpose"));
        inventory.put(2, new Product(2, "Power X", 3, "LG", "Very good battery with 2300mah and has 4g services"));
        inventory.put(3, new Product(3, "Thumbdrive", 3, "Kingston", "Trustable thumbdrive which has high data transfer rate and solid body look"));
    }
    private static int maxProductId = 4;

    Logger logger = Logger.getLogger(ProductService.class);

    @POST
    @Path("/")
    @Consumes("application/json")
    @Produces("application/text")
    public String createProduct(Product productDetails) {
        Product newproduct = new Product(maxProductId++, productDetails);
        inventory.put(newproduct.getId(), newproduct);
        return newproduct.getId().toString();
    }


    @GET
    @Path("/")
    //@PermitAll
    @Produces("application/json")
    public Collection<Product> getProducts() {
        System.out.println("getproducts");
        return inventory.values();
    }

    @GET
    @Path("/{id}")
    @Produces("application/json")
    public Product getProduct(@PathParam("id") Integer id) {
        return inventory.get(id);
    }

    @DELETE
    @Path("/{id}")
    @Produces("application/text")
    public String deleteProduct(@PathParam("id") Integer id) {
        inventory.remove(id);
        return "product deleted successfully";
    }

    @PUT
    @Path("/{id}")
    @Consumes("application/json")
    @Produces("application/text")
    public String updateProduct(@PathParam("id") Integer id, Product productDetails) {
        inventory.remove(id);
        Product updatedproduct = new Product(maxProductId++, productDetails);
        inventory.put(updatedproduct.getId(), updatedproduct);
        return updatedproduct.getId().toString();
    }


}

All the REST API methods will be intialized by the standalone servlet intializer. This has been done by extending the basic JAX-RS application class.

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/rest")
public class ApplicationMain extends Application {

}

Basic Authentication

All the REST API resource methods are protected by basic authentication that has been intialized in the front-end as HTTP options.

const HTTPOPTIONS = {
    headers: new HttpHeaders({
        'Authorization': 'Basic YWRtaW46YWRtaW4=',
    })
};

This security has been provided by SecurityFilter , which implents  ContainerRequestFilter .

All the methods before invocation take the header authorization key value and decode it for credentials. This then validates the credentials and, if successful, invokes the REST API methods. Otherwise, it returns with a failed exception.

@Provider
@Priority(2000)
public class SecurityFilter implements ContainerRequestFilter {
    private static final ServerResponse ACCESS_FORBIDDEN = new ServerResponse("NOBODY CAN ACCESS", 403, new Headers<>());
    private static final ServerResponse ACCESS_DENIED = new ServerResponse("Authorization header missing/invalid or Role Not valid", 401, new Headers<>());
    private static final ServerResponse INTERNAL_SERVER_ERROR = new ServerResponse("Authorization credentials not encoded properly", 500, new Headers<>());
    private static final String AUTHORIZATION_PROPERTY = "Authorization";
    @Override
    public void filter(ContainerRequestContext containerRequestContext) throws IOException {
        ResourceMethodInvoker methodInvoker = (ResourceMethodInvoker)
            containerRequestContext.getProperty("org.jboss.resteasy.core.ResourceMethodInvoker");
        Method method = methodInvoker.getMethod();
        if (!method.isAnnotationPresent(PermitAll.class)) {
            if (method.isAnnotationPresent(DenyAll.class)) {
                containerRequestContext.abortWith(ACCESS_FORBIDDEN);
                return;
            }
            final MultivaluedMap<String, String> headers = containerRequestContext.getHeaders();
            final List<String> authorization = headers.get(AUTHORIZATION_PROPERTY);

            if(authorization==null || authorization.isEmpty()) {
                containerRequestContext.abortWith(ACCESS_DENIED);
                return;
            }
            String credentials = null;
            try{
                credentials = new String(Base64.decode(authorization.get(0).replaceFirst("Basic ", "")));
            } catch(Exception exception) {
                containerRequestContext.abortWith(INTERNAL_SERVER_ERROR);
                return;
            }
            final StringTokenizer credentialsParser = new StringTokenizer(credentials, ":");
            String username = credentialsParser.nextToken();
            String password = credentialsParser.nextToken();
            if (username.equals("admin") && password.equals("admin")) {
                System.out.println("!!!Authenticated Successfully!!!");
                //skip authorization check as roles not mentioned.
                if (method.getAnnotation(RolesAllowed.class)==null) {
                    return;
                }
                String[] rolesAllowed = method.getAnnotation(RolesAllowed.class).value();
                boolean isAllowed = false;
                for(String role: rolesAllowed) {
                    if (role.equals("admin")) {
                        System.out.println("!!Authorized successfully!!");
                        isAllowed = true;
                        break;
                    }
                }
                if (!isAllowed) {
                    containerRequestContext.abortWith(ACCESS_DENIED);
                    return;
                }
            } else {
                containerRequestContext.abortWith(ACCESS_DENIED);
                return;
            }
        }
    }
}

CORS Handler

Angular 7 runs on port 4200, whereas RESTEasy has a different origin, so the REST API calls may break due to your browser’s CORS policy. So, to handle the cross-origin request, we need to add an Angular domain to the allowOrigin in the CORS filter.

@Provider
@Priority(4000)
public class CustomCorsFilter extends CorsFilter {

    public CustomCorsFilter() {
        this.getAllowedOrigins().add("http://localhost:4200");
    }

}

When there is cross-origin request, the browser sends a pre-flight request ( OPTIONS ) to the backend. Upon successfully getting a response from the backend, it sends the actual GET, POST, or any other calls to the REST API services. So  CORSAllowFilter   has been created as shown below and annotated with  @Provider so that it will be autoscanned by the  StandaloneServletInitializer . It also sends allow headers so the basic authentication can be sent in the actual method. It also passed the list of methods possible for the REST resource’s URL.

@Provider
@Priority(3000)
public class CorsAllowFilter implements ContainerResponseFilter {
    @Override
    public void filter(ContainerRequestContext containerRequestContext, ContainerResponseContext containerResponseContext) throws IOException {
        if (containerResponseContext.getHeaders().containsKey("Access-Control-Allow-Origin")==false || containerResponseContext.getHeaders().get("Access-Control-Allow-Origin").isEmpty()) {
            containerResponseContext.getHeaders().add("Access-Control-Allow-Origin", "http://localhost:4200");
        }
        containerResponseContext.getHeaders().add(
                "Access-Control-Allow-Credentials", "true");
        containerResponseContext.getHeaders().add(
                "Access-Control-Allow-Headers",
                "origin, content-type, accept, authorization");
        containerResponseContext.getHeaders().add(
                "Access-Control-Allow-Methods",
                "GET, POST, PUT, DELETE, OPTIONS, HEAD");
    }
}

Backend RESTEasy example can be referenced from the GitHub link .

阅读原文...


微信扫一扫,分享到朋友圈

Angular Tutorial: Angular 7 and the RESTEasy Framework
0

DZone

REST API Security: Pen Tests

上一篇

交互设计师岗位职责分析

下一篇

评论已经被关闭。

插入图片

热门分类

往期推荐

Angular Tutorial: Angular 7 and the RESTEasy Framework

长按储存图像,分享给朋友