Initial commit
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/gradle/wrapper/
|
||||
/build/
|
||||
/.idea/
|
||||
/.gradle/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
21
LICENSE.md
Normal file
21
LICENSE.md
Normal 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
51
README.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
The following form will be displayed to add a new artist when clicking on the “+” button at the bottom:
|
||||
|
||||

|
||||
|
||||
The following form will be displayed to add an album to an artist when clicking on the “+” button next to the album:
|
||||
|
||||

|
||||
|
||||
# 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.
|
||||
|
||||

|
||||
53
STRUCTURE.md
Normal file
53
STRUCTURE.md
Normal 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
48
build.gradle
Normal 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
1
settings.gradle
Normal file
@@ -0,0 +1 @@
|
||||
rootProject.name = 'albumcatalog'
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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/");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
2
src/main/resources/application-dev.properties
Normal file
2
src/main/resources/application-dev.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
server.port = 9090
|
||||
spring.h2.console.enabled = false
|
||||
2
src/main/resources/application.properties
Normal file
2
src/main/resources/application.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
server.port=8080
|
||||
spring.h2.console.enabled = false
|
||||
5
src/main/resources/data.sql
Normal file
5
src/main/resources/data.sql
Normal 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);
|
||||
BIN
src/main/resources/static/app_screen_1.png
Normal file
BIN
src/main/resources/static/app_screen_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
BIN
src/main/resources/static/app_screen_2.png
Normal file
BIN
src/main/resources/static/app_screen_2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
src/main/resources/static/app_screen_3.png
Normal file
BIN
src/main/resources/static/app_screen_3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
src/main/resources/static/app_screen_4.png
Normal file
BIN
src/main/resources/static/app_screen_4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
55
src/main/resources/static/custom.css
Normal file
55
src/main/resources/static/custom.css
Normal 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;
|
||||
}
|
||||
BIN
src/main/resources/static/favicon.png
Normal file
BIN
src/main/resources/static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
101
src/main/resources/static/scripts.js
Normal file
101
src/main/resources/static/scripts.js
Normal 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("");
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
148
src/main/resources/templates/index.html
Normal file
148
src/main/resources/templates/index.html
Normal 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">×</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">×</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>
|
||||
Reference in New Issue
Block a user