Initial commit

This commit is contained in:
2020-11-07 19:00:27 +01:00
commit 78aaf3827d
31 changed files with 978 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/gradle/wrapper/
/build/
/.idea/
/.gradle/
/gradlew
/gradlew.bat

21
LICENSE.md Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 droideparanoico
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

51
README.md Normal file
View File

@@ -0,0 +1,51 @@
# Album catalog web application
**Spring Boot** application using **Thymeleaf** view to build an album catalog capable of:
- Create a new artist
- Delete an artist
- Add a new album to an artist
- Delete an album from a artist
# Running the application
Download the project and extract the contents to a folder.
Once Java is installed, please run the following command to start the application:
**For Linux/Macintosh**
```
cd <extracted-folder>
./gradlew clean build bootRun
```
**For Windows**
```
cd <extracted-folder>
gradlew clean build bootRun
```
For deploying the application onto another web server, we can build the project using the following command and host the `albumcatalog-0.0.1-SNAPSHOT.war` that gets generated after building under `build/libs`.
**For Linux/Macintosh**
```
cd <extracted-folder>
./gradlew clean build
```
**For Windows**
```
cd <extracted-folder>
gradlew clean build
```
By default, the application will run on `8080`, which can be changed using the property `server.port` in `src/main/resources/application.properties`. To access the application UI, open [http://localhost:8080](http://localhost:8080/) on any browser.
![](src/main/resources/static/app_screen_1.png)
The following form will be displayed to add a new artist when clicking on the “+” button at the bottom:
![](src/main/resources/static/app_screen_2.png)
The following form will be displayed to add an album to an artist when clicking on the “+” button next to the album:
![](src/main/resources/static/app_screen_3.png)
# Using Swagger
To access Swagger UI, click on the Swagger icon at the top right corner. The following page will be opened where we can try out and execute the APIs.
![](src/main/resources/static/app_screen_4.png)

53
STRUCTURE.md Normal file
View File

@@ -0,0 +1,53 @@
# Project structure and files
- **build.gradle** This contains the dependencies needed for the project.
- **src/main**
* **java**
* ```com.droideparanoico.albumcatalog```
* **ServletInitializer.java** this class extends SpringBootServletInitializer to make it eligible to deploy as a traditional WAR application.
* **SpringBootAlbumCatalog.java** This class is annotated with @SpringBootApplication which does the auto-configuration of the application.
* ```com.droideparanoico.albumcatalog.config```
* **SwaggerConfiguration.java** This is a @Configuration annotated class that contains the beans that initialize Swagger for this application.
* ```com.droideparanoico.albumcatalog.controller```
* **ArtistController.java** This is a @Controller annotated classes that contains the request handler method for the following endpoints:
GET / The request handler method listArtistAndAlbums(Model) is called when we land on the root application.
* ```com.droideparanoico.albumcatalog.controller.data```
* **ArtistRestController.java** This is a @RestController annotated class that contains the request handler method for the following endpoints:
|HTTP Method|Endpoint|Request Handling Method|Description|
|:---:|:---:|:---:|---|
|```GET```|```/artist/```|```root()```|returns **“application is working!”** when called|
|```GET```|```/artist/all```|```getAllArtists()```|returns ```Iterable<Artist>``` when called|
|```GET```|```/artist/{id}```|```getArtistById(BigInteger)```|returns the ```Artist``` with the given id when called|
|```POST```|```/artist/{name}```|```createArtist(String)```|for creating a new **Artist** with the given name and returns the same after creation|
|```DELETE```|```/artist/{id}```|```deleteArtist(BigInteger)```|for deleting an **Artist** by id|
|```GET```|```/artist/{id}/albums```|```getAlbumsByArtist(BigInteger)```|for fetching all the albums of the **Artist** with the given id|
|```DELETE```|```/artist/{id}/albums```|```deleteAllAlbumsFromArtist(BigInteger)```|for deleting all the albums of the **Artist** with the given id|
|```POST```|```/artist/{id}/add```|```addAlbumToArtist(BigInteger, Album)```|for adding the given **Album** to the **Artist** with the given id and returns the created album|
|```GET```|```/artist/albums```|```getAllAlbums()```|returns ```List<Album>``` that fetches all the albums from all the artists|
|```PUT```|```/artist/albums/{id}/move```|```moveAlbumToDifferentArtist(BigInteger, BigInteger)```|for moving an album from one artist to another|
|```DELETE```|```/artist/{id}/albums/{album_id}```|```deleteFromArtist(BigInteger, BigInteger)```|for deleting an album from an artist|
> **ArtistRestController** uses **ArtistService** for business logic and interacting with persistence layer.
* ```com.droideparanoico.albumcatalog.database```
* **ArtistRepository.java** this interface extends JpaRepository for interacting with artist database table.
* **AlbumsRepository.java** this interface extends JpaRepository for interacting with album database table.
* ```com.droideparanoico.albumcatalog.exception```
* **ExceptionController.java** this class is annotated with @ControllerAdvice and contains @ExceptionHandler annotated method to handle exceptions.
* **ArtistNotFoundException.java** a custom exception class for managing an invalid artist.
* **AlbumNotFoundException.java** a custom exception class for managing an invalid album.
* ```com.droideparanoico.albumcatalog.model```
* **ErrorCodes.java** and enum that holds the error codes associated with our custom exceptions.
* **Artist.java** a POJO class for saving the artist information.
* **Album.java** a POJO class for saving the album information.
* ```com.droideparanoico.albumcatalog.service```
* **ArtistService.java** a Business Facade Service that controllers use to interact with the database repositories and contains the business logic to manipulate the data that is transferred between the controller and persistence layer.
* resources
* ```templates```
* **index.html** the HTML template with embedded Thymeleaf constructs for rendering the server-side response.
* ```static```
* **custom.css** a cascading stylesheet for our application.
* **scripts.js** a Javascript file containing the method to interact with our backend RESTful APIs that we have built.
* **application.properties** contains the Spring Boot configurations.
* **application-dev.properties** contains the Spring Boot configurations for ```dev``` profile.
* **data.sql** initializing database script that is executed at the time of application boot up.

48
build.gradle Normal file
View File

@@ -0,0 +1,48 @@
plugins {
id 'org.springframework.boot' version '2.3.5.RELEASE'
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
id 'java'
id 'war'
id 'eclipse'
id 'idea'
}
group = 'com.droideparanoico'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'com.google.guava:guava:30.0-jre'
implementation 'io.springfox:springfox-spring-webmvc:2.10.5'
implementation 'io.springfox:springfox-swagger2:2.10.5'
implementation 'io.springfox:springfox-swagger-ui:2.10.5'
compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
test {
useJUnitPlatform()
}

1
settings.gradle Normal file
View File

@@ -0,0 +1 @@
rootProject.name = 'albumcatalog'

View File

@@ -0,0 +1,13 @@
package com.droideparanoico.albumcatalog;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(SpringBootAlbumCatalog.class);
}
}

View File

@@ -0,0 +1,13 @@
package com.droideparanoico.albumcatalog;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootAlbumCatalog {
public static void main(String[] args) {
SpringApplication.run(SpringBootAlbumCatalog.class, args);
}
}

View File

@@ -0,0 +1,42 @@
package com.droideparanoico.albumcatalog.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
@Configuration
@EnableSwagger2WebMvc
public class SwaggerConfiguration {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build();
}
@Bean
public WebMvcConfigurer configureSwagger() {
return new WebMvcConfigurer() {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
};
}
}

View File

@@ -0,0 +1,22 @@
package com.droideparanoico.albumcatalog.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import com.droideparanoico.albumcatalog.service.ArtistService;
@Controller
public class ArtistController {
@Autowired
private ArtistService artistService;
@GetMapping("/")
public String listArtistAndAlbums(Model model) {
model.addAttribute("artists", artistService.getAllArtists());
return "index";
}
}

View File

@@ -0,0 +1,92 @@
package com.droideparanoico.albumcatalog.controller.data;
import java.math.BigInteger;
import java.util.Optional;
import com.droideparanoico.albumcatalog.model.Artist;
import com.droideparanoico.albumcatalog.model.Album;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.droideparanoico.albumcatalog.service.ArtistService;
@RestController
@RequestMapping("/artist")
public class ArtistRestController {
public ArtistService service;
@Qualifier("artistService")
@Autowired
public void setService(ArtistService service) {
this.service = service;
}
@GetMapping("/")
public String root() {
return "application is runnning!";
}
@GetMapping("/all")
public Iterable<Artist> getAllArtists() {
return service.getAllArtists();
}
@GetMapping("/{id}")
public Artist getArtistById(final @PathVariable("id") BigInteger artistId) {
return service.getAlbumsById(artistId);
}
@PostMapping("/{name}")
public Optional<Artist> createArtist(final @PathVariable String name) {
return service.createArtist(name);
}
@DeleteMapping("/{id}")
public void deleteArtist(final @PathVariable("id") BigInteger artistId) {
service.deleteArtist(artistId);
}
@GetMapping("/{id}/albums")
public Iterable<Album> getAlbumsByArtist(@PathVariable("id") BigInteger artistId) {
return service.getAlbums(artistId);
}
@DeleteMapping("/{id}/albums")
public void deleteAllAlbumsFromArtist(final @PathVariable("id") BigInteger artistId) {
service.deleteAlbums(artistId);
}
@PostMapping("/{id}/add")
public Album addAlbumToArtist(final @PathVariable("id") BigInteger artistId,
final @RequestBody Album album) {
return service.addAlbum(artistId, album);
}
@GetMapping("/albums")
public Iterable<Album> getAllAlbums() {
return service.getAlbums(null);
}
@PutMapping("/albums/{id}/move")
public boolean moveAlbumToDifferentArtist(@PathVariable("id") BigInteger albumId,
@RequestParam("to_artist") BigInteger toArtistId) {
return service.moveAlbum(albumId, toArtistId);
}
@DeleteMapping("/{id}/albums/{album_id}")
public void deleteFromArtist(final @PathVariable("id") BigInteger artistId,
final @PathVariable("album_id") BigInteger albumId) {
service.deleteAlbum(artistId, albumId);
}
}

View File

@@ -0,0 +1,34 @@
package com.droideparanoico.albumcatalog.database;
import java.math.BigInteger;
import java.util.Collection;
import java.util.Optional;
import com.droideparanoico.albumcatalog.model.Album;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;
public interface AlbumsRepository extends JpaRepository<Album, BigInteger> {
public Optional<Album> findByName(String name);
Collection<Album> findByArtistId(BigInteger artistId);
@Transactional
@Modifying(clearAutomatically = true)
@Query("delete from Album s where s.artistId = ?1")
void deleteByArtistId(BigInteger artistId);
@Transactional
@Modifying(clearAutomatically = true)
@Query("delete from Album s where s.artistId = ?1 and s.id = ?2")
public void delete(BigInteger artistId, BigInteger albumId);
@Transactional
@Modifying(clearAutomatically = true)
@Query("update Album s set s.artistId = ?2 where s.id = ?1")
public int updateArtist(BigInteger albumId, BigInteger artistId);
}

View File

@@ -0,0 +1,23 @@
package com.droideparanoico.albumcatalog.database;
import java.math.BigInteger;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import com.droideparanoico.albumcatalog.model.Artist;
import com.droideparanoico.albumcatalog.model.Album;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
public interface ArtistRepository extends JpaRepository<Artist, BigInteger> {
public Optional<Artist> findByName(String name);
@Query("select a from Album a where a.artistId = ?1")
public Collection<Album> getAlbums(BigInteger artistId);
@Query(value = "select name from Album where artist_id = ?", nativeQuery = true)
public List<String> getAlbumsUsingNativeQuery(BigInteger albumId);
}

View File

@@ -0,0 +1,11 @@
package com.droideparanoico.albumcatalog.exception;
import java.math.BigInteger;
@SuppressWarnings("serial")
public class AlbumNotFoundException extends RuntimeException {
public AlbumNotFoundException(final BigInteger id) {
super(String.format("album with id '%s' not found", id));
}
}

View File

@@ -0,0 +1,11 @@
package com.droideparanoico.albumcatalog.exception;
import java.math.BigInteger;
@SuppressWarnings("serial")
public class ArtistNotFoundException extends RuntimeException {
public ArtistNotFoundException(final BigInteger id) {
super(String.format("album with id '%s' not found", id));
}
}

View File

@@ -0,0 +1,34 @@
package com.droideparanoico.albumcatalog.exception;
import com.droideparanoico.albumcatalog.model.ErrorCodes;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import lombok.AllArgsConstructor;
import lombok.Data;
@ControllerAdvice
public class ExceptionController {
@ExceptionHandler
public ResponseEntity<?> artistNotFound(ArtistNotFoundException ex) {
return ResponseEntity.badRequest()
.body(new ResponseStatusError(ErrorCodes.ARTIST_NOT_FOUND.code(), ex.getMessage()));
}
@ExceptionHandler
public ResponseEntity<?> albumNotFound(AlbumNotFoundException ex) {
return ResponseEntity.badRequest()
.body(new ResponseStatusError(ErrorCodes.ALBUM_NOT_FOUND.code(), ex.getMessage()));
}
}
@Data
@AllArgsConstructor
class ResponseStatusError {
private int status;
private String message;
}

View File

@@ -0,0 +1,42 @@
package com.droideparanoico.albumcatalog.model;
import java.math.BigInteger;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedNativeQuery;
import javax.persistence.Table;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
@Entity
@Table(name = "album")
@NamedNativeQuery(name = "albumsByArtistId", query = "select id, name, artist_id, cover_url, created_on from album a where a.artist_id = ?", resultClass = Album.class)
public class Album {
@GeneratedValue(strategy = GenerationType.AUTO)
@Id
private BigInteger id;
@Column(name = "artist_id")
@JsonProperty("artist_id")
private BigInteger artistId;
private String name;
@Column(name = "cover_url")
@JsonProperty("cover_url")
private String coverUrl;
@Column(name = "created_on")
@JsonProperty("created_on")
private Date createdOn;
}

View File

@@ -0,0 +1,42 @@
package com.droideparanoico.albumcatalog.model;
import java.math.BigInteger;
import java.util.Collection;
import java.util.Date;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
@Entity
@Table(name = "artist")
public class Artist {
@GeneratedValue(strategy = GenerationType.AUTO)
@Id
private BigInteger id;
private String name;
@Column(name = "created_on")
@JsonProperty("created_on")
private Date createdOn;
@ElementCollection(targetClass = java.util.HashSet.class)
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn
private Collection<Album> albums;
}

View File

@@ -0,0 +1,18 @@
package com.droideparanoico.albumcatalog.model;
public enum ErrorCodes {
ARTIST_NOT_FOUND(1001),
ALBUM_NOT_FOUND(1002);
private int code;
ErrorCodes(int code) {
this.code = code;
}
public int code() {
return this.code;
}
}

View File

@@ -0,0 +1,88 @@
package com.droideparanoico.albumcatalog.service;
import java.math.BigInteger;
import java.util.Date;
import java.util.Optional;
import com.droideparanoico.albumcatalog.database.AlbumsRepository;
import com.droideparanoico.albumcatalog.exception.ArtistNotFoundException;
import com.droideparanoico.albumcatalog.exception.AlbumNotFoundException;
import com.droideparanoico.albumcatalog.model.Album;
import com.droideparanoico.albumcatalog.model.Artist;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.droideparanoico.albumcatalog.database.ArtistRepository;
@Service("artistService")
public class ArtistService {
@Autowired
private ArtistRepository artistRepo;
@Autowired
private AlbumsRepository albumsRepo;
public Iterable<Artist> getAllArtists() {
return artistRepo.findAll();
}
public Artist getAlbumsById(BigInteger artistId) {
return getArtist(artistId);
}
public Optional<Artist> createArtist(String name) {
Artist artist = new Artist();
artist.setName(name);
artist.setCreatedOn(new Date());
return Optional.of(artistRepo.save(artist));
}
public void deleteArtist(BigInteger artistId) {
Artist artist = getArtist(artistId);
artist.setId(artistId);
artistRepo.delete(artist);
}
public Iterable<Album> getAlbums(BigInteger artistId) {
if (artistId == null) {
return albumsRepo.findAll();
}
artistRepo.getAlbums(artistId);
Artist artist = getArtist(artistId);
return artistRepo.getAlbums(artist.getId());
}
public void deleteAlbums(BigInteger artistId) {
Artist artist = getArtist(artistId);
albumsRepo.deleteByArtistId(artist.getId());
}
public Album addAlbum(BigInteger artistId, Album album) {
Artist artist = getArtist(artistId);
album.setArtistId(artist.getId());
album.setCreatedOn(new Date());
return albumsRepo.save(album);
}
public boolean moveAlbum(BigInteger albumId, BigInteger toArtistId) {
Album album = getAlbum(albumId);
Artist artist = getArtist(toArtistId);
return 1 == albumsRepo.updateArtist(album.getId(), artist.getId());
}
public void deleteAlbum(BigInteger artistId, BigInteger albumId) {
Album album = getAlbum(albumId);
albumsRepo.delete(artistId, album.getId());
}
private Artist getArtist(final BigInteger artistId) {
return artistRepo.findById(artistId)
.orElseThrow(() -> new ArtistNotFoundException(artistId));
}
private Album getAlbum(final BigInteger albumId) {
return albumsRepo.findById(albumId).orElseThrow(() -> new AlbumNotFoundException(albumId));
}
}

View File

@@ -0,0 +1,2 @@
server.port = 9090
spring.h2.console.enabled = false

View File

@@ -0,0 +1,2 @@
server.port=8080
spring.h2.console.enabled = false

View File

@@ -0,0 +1,5 @@
create table if not exists artist(id bigint auto_increment primary key, name VARCHAR(250) NOT NULL, created_on DATE default CURRENT_TIMESTAMP );
create table if not exists album(id bigint auto_increment PRIMARY KEY, album_id bigint NOT NULL, name VARCHAR(250) NOT NULL, cover_url VARCHAR(250) NOT NULL, created_on DATE default CURRENT_TIMESTAMP, FOREIGN KEY(artist_id) REFERENCES artist(id) ON UPDATE CASCADE);
insert into artist(id, name, created_on) values(1, 'Radiohead', CURRENT_TIMESTAMP);
insert into album(id, name, artist_id, cover_url, created_on) values(2, 'OK Computer', 1, 'https://upload.wikimedia.org/wikipedia/en/b/ba/Radioheadokcomputer.png', CURRENT_TIMESTAMP);

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -0,0 +1,55 @@
.centered {
text-align: center;
}
td {
text-align: center;
vertical-align: middle;
}
.fa-3x {
vertical-align: middle;
}
.row {
margin: 2em
}
thead {
font-weight: bolder;
text-align: center
}
.table>tbody>tr>td {
vertical-align: middle;
}
.royal_blue {
background-color: #0074B7;
color: white;
font-weight: bolder;
}
.table {
margin-bottom: 0px;
}
.fa {
cursor: pointer;
}
span.fa-trash:hover {
color: red;
}
span.fa-plus:hover {
color: green;
}
.cover-url {
max-width: 200px;
max-height: 120px;
box-shadow: 2px 2px 1px rgba(50, 50, 50, 0.75);
border: 1px solid grey;
border-radius: 5px 5px 5px 5px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,101 @@
$(document).ready(function () {
$("#artist > tbody").append(
'<tr style="background-color: #0074B7" class="royal_blue"><td colspan="3"><span data-toggle="modal" data-target="#add-artist" style="color: white" title="Create artist" class="fa fa-plus fa-3x" aria-hidden="true"> </span></td></tr>'
);
function getId(obj, prefix) {
return $(obj)
.attr("class")
.match(prefix + "-[0-9]+")[0]
.split("-")[1]
.trim();
}
$(".remove-album").on("click", function (evt) {
var result = confirm("Sure you want to delete?");
if (!result) {
return;
}
let albumId = getId(this, "album");
let artistId = getId(this, "artist");
$.ajax({
url: `artist/${artistId}/albums/${albumId}`,
type: "DELETE",
success: function (data) {
alert("successfully deleted the album from artist");
window.location.reload();
},
});
});
$(".delete_artist").on("click", function (evt) {
var result = confirm("Sure you want to delete?");
if (!result) {
return;
}
let artistId = getId(this, "artist");
$.ajax({
url: `artist/${artistId}`,
type: "DELETE",
success: function (data) {
alert("successfully deleted the artist");
window.location.reload();
},
});
});
$("form.add-artist").on("submit", function (evt) {
evt.preventDefault();
let name = $("#artist-name").val().trim();
if (name == null || name.length == 0 || name.length < 3) {
alert("name cannot be null/empty/length less than 3");
return;
}
$.ajax({
url: `artist/${name}`,
type: "POST",
success: function (data) {
alert("successfully created the artist");
window.location.reload();
},
complete: function () {},
});
});
$("span[data-id]").on("click", function (evt) {
$("#artist-id").val($(this).attr("data-id"));
});
$("form.add-album").on("submit", function (evt) {
evt.preventDefault();
let name = $("#album-name").val().trim();
if (name == null || name.length == 0 || name.length < 3) {
alert("name cannot be null/empty/length less than 3");
return;
}
let url = $("#cover-url").val().trim();
let match = url.match("http.*(jpg|jpeg|gif|png)");
if (match == null || match < 0) {
alert("enter valid image url");
return;
}
let artistId = $("#artist-id").val().split("-")[1].trim();
$.ajax({
url: `artist/${artistId}/add`,
dataType: "json",
contentType: "application/json",
type: "POST",
data: JSON.stringify({
name: name,
cover_url: url,
}),
success: function (data) {
alert("successfully added album to artist");
window.location.reload();
},
complete: function () {
$("#artist-id").val("");
},
});
});
});

View File

@@ -0,0 +1,148 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Album catalog app</title>
<link rel="shortcut icon" th:href="@{/favicon.png}" type="image/x-icon">
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"/>
<link rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"/>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script
src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script th:src="@{/scripts.js}"></script>
<link rel="stylesheet" th:href="@{/custom.css}" />
</head>
<body>
<div class="container">
<div class="row">
<div class="centered">
<div class="royal_blue" ; style="margin: 0.1em">
<span style="margin: 0.1em; float: center; font-size: 2em">
Album catalog application </span> <span
style="margin: 0.25em; float: right"> <a
title="Click to Open Swagger UI" href="swagger-ui.html"><img
width="30px" height="30px"
src="https://upload.wikimedia.org/wikipedia/commons/a/ab/Swagger-logo.png"/></a>
</span>
</div>
<table id="artist"
class="table table-striped table-bordered table-responsive">
<thead class="royal_blue" style="color: white; font-size: 1.5em;">
<tr>
<td>Artist</td>
<td>Albums</td>
<td style="width: 15%"></td>
</tr>
</thead>
<tbody>
<tr th:each="artist: ${artists}">
<td class="album_id" style="display: none"
th:text="${artist.id}"></td>
<td th:text="${artist.name}"></td>
<td>
<table
class="table table-striped table-bordered table-responsive">
<tr class="table" th:each="album: ${artist.albums}">
<td class="album_id" style="display: none" th:text="${album.id}"></td>
<td th:text="${album.name}"></td>
<td><img class="cover-url" th:src="${album.coverUrl}" />
<td><span title="Remove Album"
th:classappend="${'artist-' + artist.id + ' album-' + album.id}"
class="fa fa-trash fa-3x remove-album" aria-hidden="true">
</span></td>
</tr>
</table>
</td>
<td>
<div style="margin: 0.25em; display: inline;">
<span data-toggle="modal" data-target="#add-album"
th:attr="data-id='artist-' + ${artist.id}"
style="padding: 0 0.25em" title="Add Album"
class="fa fa-plus fa-3x" aria-hidden="true"></span> <span
data-toggle="modal" title="Delete Artist"
style="padding: 0 0.25em"
th:classappend="${'artist-' + artist.id}"
class="fa fa-trash fa-3x delete_artist" aria-hidden="true">
</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="modal fade" id="add-artist" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header centered royal_blue">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 style="color: white;" class="modal-title">Add Artist</h4>
</div>
<div class="modal-body">
<form class='add-artist'>
<div class="form-group">
<table
class="table table-striped table-bordered table-responsive">
<tr>
<td><label class="form-check-label" for="artist-name">Name</label></td>
<td><input name="artist-name" id="artist-name"
class="form-control" type="text"/></td>
</tr>
<tr>
<td colspan="2">
<button type="submit"
class="btn btn-primary btn-lg add-artist">Add</button>
</td>
</tr>
</table>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="modal fade" id="add-album" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header centered royal_blue">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 style="color: white;" class="modal-title">Add Album</h4>
</div>
<div class="modal-body">
<form class='add-album'>
<div class="form-group">
<table
class="table table-striped table-bordered table-responsive">
<tr style="display: none">
<td><label class="form-check-label" for="artist-id">Name</label></td>
<td><input name="artist-id" id="artist-id"
class="form-control" type="hidden"/></td>
</tr>
<tr>
<td><label class="form-check-label" for="album-name">Name</label></td>
<td><input name="album-name" id="album-name"
class="form-control" type="text"/></td>
</tr>
<tr>
<td><label class="form-check-label" for="cover-url">Cover
URL</label></td>
<td><input name="cover-url" id="cover-url"
class="form-control" type="text"/></td>
</tr>
<tr>
<td colspan="2">
<button type="submit" class="btn btn-primary btn-lg">Add</button>
</td>
</tr>
</table>
</div>
</form>
</div>
</div>
</div>
</div>
</body>
</html>