Commit 9a3deae1 authored by Michal-MK's avatar Michal-MK
Browse files

Initial Commit containing the base for frontend seminar

parents
.idea
.settings
.project
test-output
.classpath
*.iml
*~
target
## Seminar Frontend: JavaScript, AJAX, AngularJS - Tasks
**Task 01 (project build)**
In a new folder, checkout the branch `step1` from https://gitlab.fi.muni.cz/pa165/seminar-09-frontend
and build the whole project. Then run the **eshop-angular** subproject.
```
mkdir seminar-javascript
cd seminar-javascript
git clone -b step1 https://gitlab.fi.muni.cz/pa165/seminar-09-frontend
cd seminar-09-frontend/
mvn clean install
cd eshop-angular
mvn cargo:run
```
**Task 02 (browser tools)**
The application has a **REST** (Representational State Transfer) **API** (Application Programming Interface) available.
The API conforms to the **HATEOAS** (Hypermedia as the Engine of Application State) principles seen in the previous seminar.
Moreover, the **JSON** (JavaScript Object Notation) serialization of objects conforms to the **HAL** (Hypertext Application Language) format,
which requires each object to have `_links` part linking to other resources, and collections of objects are serialized in `_embedded` part.
To see it conveniently, we will need some tools in browser.
If you use Chrome, install [JSONView for Chrome](https://chrome.google.com/webstore/detail/jsonview/chklaanhfefbnpoihckbnefhakgolnmc).
For Firefox, you may install [JSONView for Firefox](https://jsonview.com/) or use the default JSON viewer available since Firefox 57.
Visit the URL [http://localhost:8080/eshop/api/v1/categories/](http://localhost:8080/eshop/api/v1/categories/) which lists all eshop categories.
Try following the links to see the HATEOAS principles in practise.
Then visit the URL [http://localhost:8080/eshop/#!/shopping](http://localhost:8080/eshop/#!/shopping) which is an AngularJS application calling the REST API.
Press the key **F12** on your keyboard. The browser should open a sub-window with developer tools.
Clink on **Console** tab to see debugging logs from JavaScript.
**Task 03 (Javadoc)**
The implementation of the REST API needed quite a few classes and packages.
For such complex applications, documentation is helpful. Let's generate it.
In a terminal window, go to the **eshop-angular** folder and issue the following command:
```
mvn javadoc:javadoc
```
Then open the file **target/site/apidocs/index.html** in your browser.
You will see the javadoc documentation for the project.
**Note**: The main description of the project was taken from the file **src/main/javadoc/overview.html**,
and the description of each package was taken from the **package-info.java** file in each package in the **src/main/java** folder tree.
Also note that this javadoc generation was enabled in the **pom.xml** file using the **maven-javadoc-plugin**,
which is configured to link to other javadocs for classes from imported libraries.
There is a missing description for the **cz.muni.fi.pa165.restapi.exceptions** package.
Create a **package-info.java** in that package and regenerate the javadoc.
**Task 04 (jQuery AJAX)**
Let's try an AJAX call from browser to the REST API. We will use the jQuery library for that.
Visit the URL [http://localhost:8080/eshop/jquery_example.html](http://localhost:8080/eshop/jquery_example.html) and click the button. The table will get loaded with the list of available products. See the page source to see how it is implemented.
In the same page, implement equivalent functionality for categories. Use the URL http://localhost:8080/eshop/api/v1/categories for getting the list of categories in JSON format.
After making the changes, rebuild the eshop-angular module and restart Tomcat by issuing the command:
```bash
mvn package cargo:run
```
**Task 05 (AngularJS basics)**
You have already seen the URL [http://localhost:8080/eshop/#!/shopping](http://localhost:8080/eshop/#!/shopping) in the Task 02.
It is a web interface implemented as a single-page web application using framework [AngularJS](https://angularjs.org/).
See the file **src/main/webapp/index.jsp**. It is the main HTML document.
It loads three frameworks - Bootstrap, jQuery and Angular.
Then it defines a navigation menu. Please note that hyperlinks in such single-page application are not links to other HTML documents,
rather they use the fragment part of URL after the **#** character, followed by **!**,
so for example a hyperlink to a product detail is [#!/product/25](http://localhost:8080/eshop/#!/product/25).
At this moment, only two views are implemented:
* eshop overview - [/shopping](http://localhost:8080/eshop/#!/shopping)
* product detail - e.g. [/product/1](http://localhost:8080/eshop/#!/product/1)
The main page contains very little of AngularJS functionality.
It contains a DIV element with the **ng-app** attribute marking the place managed by AngularJS,
and a DIV with **ng-view** attribute which is a placeholder for changing HTML views:
```
<div ng-app="pa165eshopApp">
<div ng-view></div>
</div>
```
The page also loads the file **angular_app.js** which contains the JavaScript part of our AngularJS application.
See the source of the file.
At the start, it initializes the whole application into a variable **pa165eshopApp** and its dependency on two modules.
The first module is **ngRoute**, which provides the functionality of mapping URL fragments to HTML views.
This module is initialized right afterwards, and for each URL fragment defines the file with HTML template
and the JavaScript controller that will take care of the particular HTML view.
The second module is named **eshopControllers** and it implements our application.
At this moment, it defines two controllers:
* **ShoppingCtrl** which makes AJAX calls for the list of categories and then for the list of products in each category, the results are stored in `$scope.categories`
* **ProductDetailCtrl** which makes a single AJAX call to a product detail and stores the result in `$scope.product`
The **ShoppingCtrl** is bound to the HTML template in **partials/shopping.html**, see it source.
It uses the **ng-repeat** attribute to generate a list of categories and list of products in each category.
The markup:
```
<div ng-repeat="category in categories">
</div>
```
means that the tag together with its content will be repeated as many times as there are items in the `$scope.categories` variable
set by the associated controller, and each item will be stored in a variable named `category`.
The template then uses expressions like `{{category.name}}` to render data from variables.
In this case, the variable `category` contains the JSON object that was sent by the server as part of the collection of categories.
Similarly, the ProductDetailCtrl is bound to the HTML template **partials/product_detail.html**
**Task 06 (implement AngularJS HTML view and controller)**
Each product detail page shows links to categories in which the product is contained.
Implement the HTML view for category detail.
* create an HTML template in file **partials/category_detail.html**
* create a controller named CategoryDetailCtrl in **the angular_app.js** file that loads category data from the REST API
* set up a routing for that HTML template and controller bound to URl fragment `/category/:categoryId`
All the functionality is already implemented as part of the ShoppingCtrl and its template, so you can copy it from there.
**Task 07 (AngularJS forms)**
Check out the branch step2 from git:
```
git checkout -f step2
```
The following changes appeared:
* files **partials/admin_products.html** (list of products) and **partials/admin_new_product.html** (form for adding a new product)
* **angular_app.js** now contains AdminProductsCtrl and AdminNewProductCtrl controllers
* routing for [/admin/products](http://localhost:8080/eshop/#!/admin/products) for AdminProductsCtrl was added
* routing for [/admin/newproduct](http://localhost:8080/eshop/#!/admin/newproduct) for AdminNewProductCtrl was added
* **index.jsp** now contains Bootstrap alerts for displaying messages and angular_app.js contains definitions of functions for closing the alerts, e.g. `hideSuccessAlert()`
* **angular_app.js** now contains definition of a new directive convertToInt which handles HTML attribute convert-to-int
used in **admin_new_product.html**, it is necessary to convert category ids from strings to integers
Please note that to keep the example simple, no authentication is implemented.
Try the following:
- Rebuild the application and visit the page [/admin/products](http://localhost:8080/eshop/#!/admin/products).
- Delete the first product.
- Delete the product with id 8.
- See the messages.
- See the implementation of AdminProductsCtrl to see how the **deleteProduct** function is implemented.
- Visit the page [/admin/newproduct](http://localhost:8080/eshop/#!/admin/newproduct) and create a new product.
- See how values in form fields are validated on the client.
See the implementation of AdminNewProductCtrl. It prepares data for the form and stores them in
the `colors`, `currencies`, `categories` and `product` variables in `$scope`.
It also defines the function `create` that is called when the form submission button is clicked.
See the form in **partials/admin_new_products.html**. It uses the following AngularJS attributes:
* the **ng-model** attribute to bind form fields to variables from $scope
* the attributes like **ng-minlength** to add validation
* the attribute **ng-show** to conditionally display error messages
* the attribute **ng-class** to mark form fields in error state with Bootstrap's `has-error` class
**Task 08 (implement form)**
Using the code described in the previous task as an example, implement pages for:
* listing of categories
* form for creating a new category
**Solution**
You can see the complete solution in the branch [solution](https://gitlab.fi.muni.cz/pa165/seminar-09-frontend/-/tree/solution).
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cz.muni.fi.pa165</groupId>
<artifactId>eshop-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>eshop-angular</artifactId>
<packaging>war</packaging>
<name>Web front end implemented in AngularJS and REST</name>
<developers>
<developer>
<name>Martin Kuba</name>
<email>makub@ics.muni.cz</email>
<organization>ÚVT MU Brno</organization>
</developer>
</developers>
<dependencies>
<!-- dependency on eshop sample data, other eshop parts are imported by transitive dependencies of this one-->
<dependency>
<groupId>cz.muni.fi.pa165</groupId>
<artifactId>eshop-sample-data</artifactId>
<version>${project.parent.version}</version>
</dependency>
<!-- servlet, JSP, JSTL -->
<!-- must be this instead of javaee-web for the springmvc-tests to succeed -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-api</artifactId>
<version>${tomcat.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.taglibs</groupId>
<artifactId>taglibs-standard-spec</artifactId>
</dependency>
<dependency>
<groupId>org.apache.taglibs</groupId>
<artifactId>taglibs-standard-impl</artifactId>
</dependency>
<!-- Spring MVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>${json-path.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<!-- must provide logging implementation, this is a runnable project -->
<!-- see viz http://docs.spring.io/platform/docs/current/reference/htmlsingle/#getting-started-logging -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- libraries needed for unit tests -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<!-- what to do when only "mvn" is run -->
<defaultGoal>compile cargo:run</defaultGoal>
<!-- name of the produced war and the context path in URL -->
<finalName>eshop</finalName>
<plugins>
<!-- embedded tomcat -->
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven3-plugin</artifactId>
</plugin>
<!-- Try "mvn javadoc:javadoc" and see target/site/apidocs -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<release>${java.version}</release>
<javadocExecutable>${java.home}/bin/javadoc</javadocExecutable>
<detectLinks>false</detectLinks>
<links>
<link>https://javaee.github.io/javaee-spec/javadocs/</link>
<link>https://docs.spring.io/spring/docs/current/javadoc-api/</link>
<link>https://docs.spring.io/spring-hateoas/docs/current/api/</link>
<link>https://fasterxml.github.io/jackson-annotations/javadoc/2.11/</link>
<link>https://docs.oracle.com/en/java/javase/11/docs/api/</link>
</links>
</configuration>
</plugin>
</plugins>
</build>
</project>
package cz.muni.fi.pa165.restapi.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import cz.muni.fi.pa165.sampledata.EshopWithSampleDataConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.validation.Validator;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Locale;
import static org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
/**
* Configures a REST application with HATEOAS responses using HAL format. See
* <ul>
* <li><a href="http://docs.spring.io/spring-hateoas/docs/current/reference/html/">Spring HATEOAS</a></li>
* <li><a href="https://apigility.org/documentation/api-primer/halprimer">Hypertext Application Language (HAL)</a></li>
* <li><a href="https://en.wikipedia.org/wiki/Hypertext_Application_Language">Hypertext Application Language (Wikipedia)</a></li>
* </ul>
* Controllers responses use the content-type "application/hal+json", the response is a JSON object
* with "_links" property for entities, or with "_links" and "_embedded" properties for collections.
*
* @author Martin Kuba makub@ics.muni.cz
*/
@EnableHypermediaSupport(type = HypermediaType.HAL)
@EnableWebMvc
@Configuration
@Import({EshopWithSampleDataConfiguration.class})
@ComponentScan(basePackages = {"cz.muni.fi.pa165.restapi.controllers", "cz.muni.fi.pa165.restapi.hateoas"})
public class RestSpringMvcConfig implements WebMvcConfigurer {
@Bean
public MappingJackson2HttpMessageConverter customJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
jsonConverter.setObjectMapper(objectMapper());
return jsonConverter;
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(customJackson2HttpMessageConverter());
}
// See http://stackoverflow.com/questions/25709672/how-to-change-hal-links-format-using-spring-hateoas
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer c) {
c.defaultContentType(MediaTypes.HAL_JSON);
}
@Bean
public ObjectMapper objectMapper() {
// Configuring mapper for HAL
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH));
return objectMapper;
}
/**
* Provides JSR-303 Validator.
*
* @return JSR-303 validator
*/
@Bean
public Validator validator() {
return new LocalValidatorFactoryBean();
}
}
package cz.muni.fi.pa165.restapi.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
/**
* Replaces web.xml file.
* Extends the class {@link AbstractAnnotationConfigDispatcherServletInitializer} that
* <ul>
* <li>creates spring context specified in the class returned by {@link #getRootConfigClasses()}</li>
* <li>initializes {@link org.springframework.web.servlet.DispatcherServlet Spring MVC dispatcher servlet} with it</li>
* <li>maps dispatcher servlet to URL pattern returned by {@link #getServletMappings()}</li>
* </ul>
*
* @author Martin Kuba makub@ics.muni.cz
*/
public class RestStartInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{RestSpringMvcConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/api/v1/*"};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return null;
}
}
/**
* This package contains classes which configure SpringMVC and Spring context.
*
* @author Martin Kuba makub@ics.muni.cz
*/
package cz.muni.fi.pa165.restapi.config;
package cz.muni.fi.pa165.restapi.controllers;
import cz.fi.muni.pa165.dto.CategoryCreateDTO;
import cz.fi.muni.pa165.dto.CategoryDTO;
import cz.fi.muni.pa165.dto.ProductDTO;
import cz.fi.muni.pa165.facade.CategoryFacade;
import cz.fi.muni.pa165.facade.ProductFacade;
import cz.muni.fi.pa165.restapi.exceptions.InvalidRequestException;
import cz.muni.fi.pa165.restapi.exceptions.ResourceNotFoundException;
import cz.muni.fi.pa165.restapi.hateoas.CategoryRepresentationModelAssembler;
import cz.muni.fi.pa165.restapi.hateoas.ProductRepresentationModelAssembler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.server.EntityLinks;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.server.ExposesResourceFor;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
/**
* SpringMVC controller for managing REST requests for the category resources. Conforms to HATEOAS principles.
*
* @author Martin Kuba makub@ics.muni.cz
*/
@RestController
@ExposesResourceFor(CategoryDTO.class)
@RequestMapping("/categories")
public class CategoriesRestController {
private final static Logger log = LoggerFactory.getLogger(CategoriesRestController.class);
public CategoriesRestController(
@Autowired ProductFacade productFacade,
@Autowired CategoryFacade categoryFacade,
@Autowired CategoryRepresentationModelAssembler categoryRepresentationModelAssembler,
@Autowired ProductRepresentationModelAssembler productRepresentationModelAssembler,
@SuppressWarnings("SpringJavaAutowiringInspection")
@Autowired EntityLinks entityLinks
) {
this.productFacade = productFacade;
this.categoryFacade = categoryFacade;
this.categoryRepresentationModelAssembler = categoryRepresentationModelAssembler;
this.productRepresentationModelAssembler = productRepresentationModelAssembler;
this.entityLinks = entityLinks;
}
private ProductFacade productFacade;
private CategoryFacade categoryFacade;
private CategoryRepresentationModelAssembler categoryRepresentationModelAssembler;
private ProductRepresentationModelAssembler productRepresentationModelAssembler;
private EntityLinks entityLinks;
/**
* Produces list of all categories in JSON.
*
* @return list of categories
*/
@RequestMapping(method = RequestMethod.GET)
public HttpEntity<CollectionModel<EntityModel<CategoryDTO>>> categories() {
log.debug("rest categories()");
List<CategoryDTO> allCategories = categoryFacade.getAllCategories();
CollectionModel<EntityModel<CategoryDTO>> categoriesCollectionModel = categoryRepresentationModelAssembler.toCollectionModel(allCategories);
categoriesCollectionModel.add(linkTo(CategoriesRestController.class).withSelfRel());
categoriesCollectionModel.add(linkTo(CategoriesRestController.class).slash("/create").withRel("create"));
return new ResponseEntity<>(categoriesCollectionModel, HttpStatus.OK);
}
/**
* Produces category detail.
*
* @param id category identifier
* @return category detail
* @throws Exception if category not found
*/
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public HttpEntity<EntityModel<CategoryDTO>> category(@PathVariable("id") long id) throws Exception {
log.debug("rest category({})", id);
CategoryDTO categoryDTO = categoryFacade.getCategoryById(id);
if (categoryDTO == null) throw new ResourceNotFoundException("category " + id + " not found");
EntityModel<CategoryDTO> categoryModel = categoryRepresentationModelAssembler.toModel(categoryDTO);
return new HttpEntity<>(categoryModel);
}
/**
* Produces a list of products in the given category.
*
* @param id category identifier
* @return list of products in the category
*/
@RequestMapping(value = "/{id}/products", method = RequestMethod.GET)
public HttpEntity<CollectionModel<EntityModel<ProductDTO>>> products(@PathVariable("id") long id) {
log.debug("rest category/{}/products()", id);
CategoryDTO categoryDTO = categoryFacade.getCategoryById(id);
if (categoryDTO == null) throw new ResourceNotFoundException("category " + id + " not found");
List<ProductDTO> products = productFacade.getProductsByCategory(categoryDTO.getName());
CollectionModel<EntityModel<ProductDTO>> productsCollectionModel = productRepresentationModelAssembler.toCollectionModel(products);
productsCollectionModel.add(entityLinks.linkForItemResource(CategoryDTO.class, id).slash("/products").withSelfRel());
return new ResponseEntity<>(productsCollectionModel, HttpStatus.OK);
}
/**
* Creates a new category.
*
* @param categoryCreateDTO DTO object containing category name
* @return newly created category
* @throws Exception if something goes wrong
*/
@RequestMapping(value = "/create", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public final HttpEntity<EntityModel<CategoryDTO>> createProduct(@RequestBody @Valid CategoryCreateDTO categoryCreateDTO, BindingResult bindingResult) throws Exception {
log.debug("rest createCategory()");
if (bindingResult.hasErrors()) {
log.error("failed validation {}", bindingResult);
throw new InvalidRequestException("Failed validation");
}
Long id = categoryFacade.createCategory(categoryCreateDTO);
EntityModel<CategoryDTO> categoryModel = categoryRepresentationModelAssembler.toModel(categoryFacade.getCategoryById(id));
return new ResponseEntity<>(categoryModel, HttpStatus.OK);
}
}
package cz.muni.fi.pa165.restapi.controllers;
import cz.muni.fi.pa165.restapi.exceptions.ErrorResource;
import cz.muni.fi.pa165.restapi.exceptions.ResourceAlreadyExistingException;
import cz.muni.fi.pa165.restapi.exceptions.ResourceNotFoundException;