commit 6e9146a00e8d59a1650e198fbcc84b0bbb4507f2 Author: Florian Date: Tue Oct 28 09:18:51 2025 +0100 Initial commit diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..96a53c7 --- /dev/null +++ b/pom.xml @@ -0,0 +1,129 @@ + + + 4.0.0 + + org.springframework.samples.petclinic.client + spring-petclinic-customers-service + jar + Spring PetClinic Customers Service + + + org.springframework.samples + spring-petclinic-microservices + 3.4.1 + + + + 8081 + ${basedir}/../docker + + + + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.cloud + spring-cloud-starter-config + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + com.mysql + mysql-connector-j + runtime + + + org.hsqldb + hsqldb + runtime + + + org.jolokia + jolokia-core + + + io.micrometer + micrometer-registry-prometheus + + + de.codecentric + chaos-monkey-spring-boot + + + io.opentelemetry + opentelemetry-exporter-zipkin + + + io.micrometer + micrometer-observation + + + io.micrometer + micrometer-tracing-bridge-brave + + + io.zipkin.reporter2 + zipkin-reporter-brave + + + net.ttddyy.observation + datasource-micrometer-spring-boot + 1.0.2 + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + + + + buildDocker + + + + org.codehaus.mojo + exec-maven-plugin + + + + + + diff --git a/src/main/java/org/springframework/samples/petclinic/customers/CustomersServiceApplication.java b/src/main/java/org/springframework/samples/petclinic/customers/CustomersServiceApplication.java new file mode 100644 index 0000000..867cdc1 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/customers/CustomersServiceApplication.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.samples.petclinic.customers; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * @author Maciej Szarlinski + */ +@EnableDiscoveryClient +@SpringBootApplication +public class CustomersServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(CustomersServiceApplication.class, args); + } +} diff --git a/src/main/java/org/springframework/samples/petclinic/customers/config/MetricConfig.java b/src/main/java/org/springframework/samples/petclinic/customers/config/MetricConfig.java new file mode 100644 index 0000000..fb2acaf --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/customers/config/MetricConfig.java @@ -0,0 +1,22 @@ +package org.springframework.samples.petclinic.customers.config; + +import io.micrometer.core.aop.TimedAspect; +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MetricConfig { + + @Bean + MeterRegistryCustomizer metricsCommonTags() { + return registry -> registry.config().commonTags("application", "petclinic"); + } + + @Bean + TimedAspect timedAspect(MeterRegistry registry) { + return new TimedAspect(registry); + } + +} diff --git a/src/main/java/org/springframework/samples/petclinic/customers/model/Owner.java b/src/main/java/org/springframework/samples/petclinic/customers/model/Owner.java new file mode 100644 index 0000000..2a7adc7 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/customers/model/Owner.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.samples.petclinic.customers.model; + +import jakarta.persistence.*; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.NotBlank; +import org.springframework.beans.support.MutableSortDefinition; +import org.springframework.beans.support.PropertyComparator; +import org.springframework.core.style.ToStringCreator; + +import java.util.*; + +/** + * Simple JavaBean domain object representing an owner. + * + * @author Ken Krebs + * @author Juergen Hoeller + * @author Sam Brannen + * @author Michael Isvy + * @author Maciej Szarlinski + * @author Ramazan Sakin + */ +@Entity +@Table(name = "owners") +public class Owner { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(name = "first_name") + @NotBlank + private String firstName; + + @Column(name = "last_name") + @NotBlank + private String lastName; + + @Column(name = "address") + @NotBlank + private String address; + + @Column(name = "city") + @NotBlank + private String city; + + @Column(name = "telephone") + @NotBlank + @Digits(fraction = 0, integer = 12) + private String telephone; + + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "owner") + private Set pets; + + protected Set getPetsInternal() { + if (this.pets == null) { + this.pets = new HashSet<>(); + } + return this.pets; + } + + public List getPets() { + final List sortedPets = new ArrayList<>(getPetsInternal()); + PropertyComparator.sort(sortedPets, new MutableSortDefinition("name", true, true)); + return Collections.unmodifiableList(sortedPets); + } + + public void addPet(Pet pet) { + getPetsInternal().add(pet); + pet.setOwner(this); + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("id", this.getId()) + .append("lastName", this.getLastName()) + .append("firstName", this.getFirstName()) + .append("address", this.address) + .append("city", this.city) + .append("telephone", this.telephone) + .toString(); + } + + public Integer getId() { + return this.id; + } + + public String getFirstName() { + return this.firstName; + } + + public String getLastName() { + return this.lastName; + } + + public String getAddress() { + return this.address; + } + + public String getCity() { + return this.city; + } + + public String getTelephone() { + return this.telephone; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public void setAddress(String address) { + this.address = address; + } + + public void setCity(String city) { + this.city = city; + } + + public void setTelephone(String telephone) { + this.telephone = telephone; + } +} diff --git a/src/main/java/org/springframework/samples/petclinic/customers/model/OwnerRepository.java b/src/main/java/org/springframework/samples/petclinic/customers/model/OwnerRepository.java new file mode 100644 index 0000000..aa720f2 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/customers/model/OwnerRepository.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.samples.petclinic.customers.model; + +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * Repository class for Owner domain objects All method names are compliant with Spring Data naming + * conventions so this interface can easily be extended for Spring Data See here: http://static.springsource.org/spring-data/jpa/docs/current/reference/html/jpa.repositories.html#jpa.query-methods.query-creation + * + * @author Ken Krebs + * @author Juergen Hoeller + * @author Sam Brannen + * @author Michael Isvy + * @author Maciej Szarlinski + */ +public interface OwnerRepository extends JpaRepository { } diff --git a/src/main/java/org/springframework/samples/petclinic/customers/model/Pet.java b/src/main/java/org/springframework/samples/petclinic/customers/model/Pet.java new file mode 100644 index 0000000..029b804 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/customers/model/Pet.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.samples.petclinic.customers.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import org.springframework.core.style.ToStringCreator; + +import java.util.Date; +import java.util.Objects; + +/** + * Simple business object representing a pet. + * + * @author Ken Krebs + * @author Juergen Hoeller + * @author Sam Brannen + * @author Maciej Szarlinski + * @author Ramazan Sakin + */ +@Entity +@Table(name = "pets") +public class Pet { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(name = "name") + private String name; + + @Column(name = "birth_date") + @Temporal(TemporalType.DATE) + private Date birthDate; + + @ManyToOne + @JoinColumn(name = "type_id") + private PetType type; + + @ManyToOne + @JoinColumn(name = "owner_id") + @JsonIgnore + private Owner owner; + + @Override + public String toString() { + return new ToStringCreator(this) + .append("id", this.getId()) + .append("name", this.getName()) + .append("birthDate", this.getBirthDate()) + .append("type", this.getType().getName()) + .append("ownerFirstname", this.getOwner().getFirstName()) + .append("ownerLastname", this.getOwner().getLastName()) + .toString(); + } + + public Integer getId() { + return this.id; + } + + public String getName() { + return this.name; + } + + public Date getBirthDate() { + return this.birthDate; + } + + public PetType getType() { + return this.type; + } + + public Owner getOwner() { + return this.owner; + } + + public void setId(Integer id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public void setBirthDate(Date birthDate) { + this.birthDate = birthDate; + } + + public void setType(PetType type) { + this.type = type; + } + + public void setOwner(Owner owner) { + this.owner = owner; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Pet pet = (Pet) o; + return Objects.equals(id, pet.id) + && Objects.equals(name, pet.name) + && Objects.equals(birthDate, pet.birthDate) + && Objects.equals(type, pet.type) + && Objects.equals(owner, pet.owner); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, birthDate, type, owner); + } +} diff --git a/src/main/java/org/springframework/samples/petclinic/customers/model/PetRepository.java b/src/main/java/org/springframework/samples/petclinic/customers/model/PetRepository.java new file mode 100644 index 0000000..5bd16db --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/customers/model/PetRepository.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.samples.petclinic.customers.model; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +/** + * Repository class for Pet domain objects All method names are compliant with Spring Data naming + * conventions so this interface can easily be extended for Spring Data See here: http://static.springsource.org/spring-data/jpa/docs/current/reference/html/jpa.repositories.html#jpa.query-methods.query-creation + * + * @author Ken Krebs + * @author Juergen Hoeller + * @author Sam Brannen + * @author Michael Isvy + * @author Maciej Szarlinski + */ +public interface PetRepository extends JpaRepository { + + /** + * Retrieve all {@link PetType}s from the data store. + * @return a Collection of {@link PetType}s. + */ + @Query("SELECT ptype FROM PetType ptype ORDER BY ptype.name") + List findPetTypes(); + + @Query("FROM PetType ptype WHERE ptype.id = :typeId") + Optional findPetTypeById(@Param("typeId") int typeId); + + +} + diff --git a/src/main/java/org/springframework/samples/petclinic/customers/model/PetType.java b/src/main/java/org/springframework/samples/petclinic/customers/model/PetType.java new file mode 100644 index 0000000..f390ef8 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/customers/model/PetType.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.samples.petclinic.customers.model; + +import jakarta.persistence.*; + +/** + * @author Juergen Hoeller + * @author Ramazan Sakin + * Can be Cat, Dog, Hamster... + */ +@Entity +@Table(name = "types") +public class PetType { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(name = "name") + private String name; + + public Integer getId() { + return this.id; + } + + public String getName() { + return this.name; + } + + public void setId(Integer id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/org/springframework/samples/petclinic/customers/web/OwnerRequest.java b/src/main/java/org/springframework/samples/petclinic/customers/web/OwnerRequest.java new file mode 100644 index 0000000..cf0e457 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/customers/web/OwnerRequest.java @@ -0,0 +1,14 @@ +package org.springframework.samples.petclinic.customers.web; + +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.NotBlank; + +public record OwnerRequest(@NotBlank String firstName, + @NotBlank String lastName, + @NotBlank String address, + @NotBlank String city, + @NotBlank + @Digits(fraction = 0, integer = 12) + String telephone +) { +} diff --git a/src/main/java/org/springframework/samples/petclinic/customers/web/OwnerResource.java b/src/main/java/org/springframework/samples/petclinic/customers/web/OwnerResource.java new file mode 100644 index 0000000..5b5a531 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/customers/web/OwnerResource.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.samples.petclinic.customers.web; + +import io.micrometer.core.annotation.Timed; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.samples.petclinic.customers.web.mapper.OwnerEntityMapper; +import org.springframework.samples.petclinic.customers.model.Owner; +import org.springframework.samples.petclinic.customers.model.OwnerRepository; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Optional; + +/** + * @author Juergen Hoeller + * @author Ken Krebs + * @author Arjen Poutsma + * @author Michael Isvy + * @author Maciej Szarlinski + */ +@RequestMapping("/owners") +@RestController +@Timed("petclinic.owner") +class OwnerResource { + + private static final Logger log = LoggerFactory.getLogger(OwnerResource.class); + + private final OwnerRepository ownerRepository; + private final OwnerEntityMapper ownerEntityMapper; + + OwnerResource(OwnerRepository ownerRepository, OwnerEntityMapper ownerEntityMapper) { + this.ownerRepository = ownerRepository; + this.ownerEntityMapper = ownerEntityMapper; + } + + /** + * Create Owner + */ + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public Owner createOwner(@Valid @RequestBody OwnerRequest ownerRequest) { + Owner owner = ownerEntityMapper.map(new Owner(), ownerRequest); + return ownerRepository.save(owner); + } + + /** + * Read single Owner + */ + @GetMapping(value = "/{ownerId}") + public Optional findOwner(@PathVariable("ownerId") @Min(1) int ownerId) { + return ownerRepository.findById(ownerId); + } + + /** + * Read List of Owners + */ + @GetMapping + public List findAll() { + return ownerRepository.findAll(); + } + + /** + * Update Owner + */ + @PutMapping(value = "/{ownerId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void updateOwner(@PathVariable("ownerId") @Min(1) int ownerId, @Valid @RequestBody OwnerRequest ownerRequest) { + final Owner ownerModel = ownerRepository.findById(ownerId).orElseThrow(() -> new ResourceNotFoundException("Owner " + ownerId + " not found")); + + ownerEntityMapper.map(ownerModel, ownerRequest); + log.info("Saving owner {}", ownerModel); + ownerRepository.save(ownerModel); + } +} diff --git a/src/main/java/org/springframework/samples/petclinic/customers/web/PetDetails.java b/src/main/java/org/springframework/samples/petclinic/customers/web/PetDetails.java new file mode 100644 index 0000000..6726228 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/customers/web/PetDetails.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.samples.petclinic.customers.web; + +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.samples.petclinic.customers.model.Pet; +import org.springframework.samples.petclinic.customers.model.PetType; + +import java.util.Date; + +/** + * @author mszarlinski@bravurasolutions.com on 2016-12-05. + */ + +record PetDetails( + + long id, + + String name, + + String owner, + + @DateTimeFormat(pattern = "yyyy-MM-dd") + Date birthDate, + + PetType type +) { + public PetDetails(Pet pet) { + this(pet.getId(), pet.getName(), pet.getOwner().getFirstName() + " " + pet.getOwner().getLastName(), pet.getBirthDate(), pet.getType()); + } +} diff --git a/src/main/java/org/springframework/samples/petclinic/customers/web/PetRequest.java b/src/main/java/org/springframework/samples/petclinic/customers/web/PetRequest.java new file mode 100644 index 0000000..b03e3a3 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/customers/web/PetRequest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.samples.petclinic.customers.web; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.Size; + +import java.util.Date; + +/** + * @author mszarlinski@bravurasolutions.com on 2016-12-05. + */ +record PetRequest(int id, + @JsonFormat(pattern = "yyyy-MM-dd") + Date birthDate, + @Size(min = 1) + String name, + int typeId +) { + +} diff --git a/src/main/java/org/springframework/samples/petclinic/customers/web/PetResource.java b/src/main/java/org/springframework/samples/petclinic/customers/web/PetResource.java new file mode 100644 index 0000000..204c1a7 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/customers/web/PetResource.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.samples.petclinic.customers.web; + +import io.micrometer.core.annotation.Timed; +import jakarta.validation.constraints.Min; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.samples.petclinic.customers.model.*; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * @author Juergen Hoeller + * @author Ken Krebs + * @author Arjen Poutsma + * @author Maciej Szarlinski + * @author Ramazan Sakin + */ +@RestController +@Timed("petclinic.pet") +class PetResource { + + private static final Logger log = LoggerFactory.getLogger(PetResource.class); + + private final PetRepository petRepository; + private final OwnerRepository ownerRepository; + + PetResource(PetRepository petRepository, OwnerRepository ownerRepository) { + this.petRepository = petRepository; + this.ownerRepository = ownerRepository; + } + + @GetMapping("/petTypes") + public List getPetTypes() { + return petRepository.findPetTypes(); + } + + @PostMapping("/owners/{ownerId}/pets") + @ResponseStatus(HttpStatus.CREATED) + public Pet processCreationForm( + @RequestBody PetRequest petRequest, + @PathVariable("ownerId") @Min(1) int ownerId) { + + Owner owner = ownerRepository.findById(ownerId) + .orElseThrow(() -> new ResourceNotFoundException("Owner " + ownerId + " not found")); + + final Pet pet = new Pet(); + owner.addPet(pet); + return save(pet, petRequest); + } + + @PutMapping("/owners/*/pets/{petId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void processUpdateForm(@RequestBody PetRequest petRequest) { + int petId = petRequest.id(); + Pet pet = findPetById(petId); + save(pet, petRequest); + } + + private Pet save(final Pet pet, final PetRequest petRequest) { + + pet.setName(petRequest.name()); + pet.setBirthDate(petRequest.birthDate()); + + petRepository.findPetTypeById(petRequest.typeId()) + .ifPresent(pet::setType); + + log.info("Saving pet {}", pet); + return petRepository.save(pet); + } + + @GetMapping("owners/*/pets/{petId}") + public PetDetails findPet(@PathVariable("petId") int petId) { + Pet pet = findPetById(petId); + return new PetDetails(pet); + } + + + private Pet findPetById(int petId) { + return petRepository.findById(petId) + .orElseThrow(() -> new ResourceNotFoundException("Pet " + petId + " not found")); + } + +} diff --git a/src/main/java/org/springframework/samples/petclinic/customers/web/ResourceNotFoundException.java b/src/main/java/org/springframework/samples/petclinic/customers/web/ResourceNotFoundException.java new file mode 100644 index 0000000..c321a99 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/customers/web/ResourceNotFoundException.java @@ -0,0 +1,13 @@ +package org.springframework.samples.petclinic.customers.web; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.NOT_FOUND) +public class ResourceNotFoundException extends RuntimeException { + + public ResourceNotFoundException(String message) { + super(message); + } + +} diff --git a/src/main/java/org/springframework/samples/petclinic/customers/web/mapper/Mapper.java b/src/main/java/org/springframework/samples/petclinic/customers/web/mapper/Mapper.java new file mode 100644 index 0000000..8b32efe --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/customers/web/mapper/Mapper.java @@ -0,0 +1,5 @@ +package org.springframework.samples.petclinic.customers.web.mapper; + +public interface Mapper { + E map(E response, R request); +} diff --git a/src/main/java/org/springframework/samples/petclinic/customers/web/mapper/OwnerEntityMapper.java b/src/main/java/org/springframework/samples/petclinic/customers/web/mapper/OwnerEntityMapper.java new file mode 100644 index 0000000..06779ec --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/customers/web/mapper/OwnerEntityMapper.java @@ -0,0 +1,19 @@ +package org.springframework.samples.petclinic.customers.web.mapper; + +import org.springframework.samples.petclinic.customers.model.Owner; +import org.springframework.samples.petclinic.customers.web.OwnerRequest; +import org.springframework.stereotype.Component; + +@Component +public class OwnerEntityMapper implements Mapper { + // This is done by hand for simplicity purpose. In a real life use-case we should consider using MapStruct. + @Override + public Owner map(final Owner owner, final OwnerRequest request) { + owner.setAddress(request.address()); + owner.setCity(request.city()); + owner.setTelephone(request.telephone()); + owner.setFirstName(request.firstName()); + owner.setLastName(request.lastName()); + return owner; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..7c95adf --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,13 @@ +spring: + application: + name: customers-service + config: + import: optional:configserver:${CONFIG_SERVER_URL:http://localhost:8888/} + + +--- +spring: + config: + activate: + on-profile: docker + import: configserver:http://config-server:8888 diff --git a/src/main/resources/db/hsqldb/data.sql b/src/main/resources/db/hsqldb/data.sql new file mode 100644 index 0000000..676b55f --- /dev/null +++ b/src/main/resources/db/hsqldb/data.sql @@ -0,0 +1,31 @@ +INSERT INTO types VALUES (1, 'cat'); +INSERT INTO types VALUES (2, 'dog'); +INSERT INTO types VALUES (3, 'lizard'); +INSERT INTO types VALUES (4, 'snake'); +INSERT INTO types VALUES (5, 'bird'); +INSERT INTO types VALUES (6, 'hamster'); + +INSERT INTO owners VALUES (1, 'George', 'Franklin', '110 W. Liberty St.', 'Madison', '6085551023'); +INSERT INTO owners VALUES (2, 'Betty', 'Davis', '638 Cardinal Ave.', 'Sun Prairie', '6085551749'); +INSERT INTO owners VALUES (3, 'Eduardo', 'Rodriquez', '2693 Commerce St.', 'McFarland', '6085558763'); +INSERT INTO owners VALUES (4, 'Harold', 'Davis', '563 Friendly St.', 'Windsor', '6085553198'); +INSERT INTO owners VALUES (5, 'Peter', 'McTavish', '2387 S. Fair Way', 'Madison', '6085552765'); +INSERT INTO owners VALUES (6, 'Jean', 'Coleman', '105 N. Lake St.', 'Monona', '6085552654'); +INSERT INTO owners VALUES (7, 'Jeff', 'Black', '1450 Oak Blvd.', 'Monona', '6085555387'); +INSERT INTO owners VALUES (8, 'Maria', 'Escobito', '345 Maple St.', 'Madison', '6085557683'); +INSERT INTO owners VALUES (9, 'David', 'Schroeder', '2749 Blackhawk Trail', 'Madison', '6085559435'); +INSERT INTO owners VALUES (10, 'Carlos', 'Estaban', '2335 Independence La.', 'Waunakee', '6085555487'); + +INSERT INTO pets VALUES (1, 'Leo', '2010-09-07', 1, 1); +INSERT INTO pets VALUES (2, 'Basil', '2012-08-06', 6, 2); +INSERT INTO pets VALUES (3, 'Rosy', '2011-04-17', 2, 3); +INSERT INTO pets VALUES (4, 'Jewel', '2010-03-07', 2, 3); +INSERT INTO pets VALUES (5, 'Iggy', '2010-11-30', 3, 4); +INSERT INTO pets VALUES (6, 'George', '2010-01-20', 4, 5); +INSERT INTO pets VALUES (7, 'Samantha', '2012-09-04', 1, 6); +INSERT INTO pets VALUES (8, 'Max', '2012-09-04', 1, 6); +INSERT INTO pets VALUES (9, 'Lucky', '2011-08-06', 5, 7); +INSERT INTO pets VALUES (10, 'Mulligan', '2007-02-24', 2, 8); +INSERT INTO pets VALUES (11, 'Freddy', '2010-03-09', 5, 9); +INSERT INTO pets VALUES (12, 'Lucky', '2010-06-24', 2, 10); +INSERT INTO pets VALUES (13, 'Sly', '2012-06-08', 1, 10); diff --git a/src/main/resources/db/hsqldb/schema.sql b/src/main/resources/db/hsqldb/schema.sql new file mode 100644 index 0000000..33d42ee --- /dev/null +++ b/src/main/resources/db/hsqldb/schema.sql @@ -0,0 +1,30 @@ +DROP TABLE pets IF EXISTS; +DROP TABLE types IF EXISTS; +DROP TABLE owners IF EXISTS; + +CREATE TABLE types ( + id INTEGER IDENTITY PRIMARY KEY, + name VARCHAR(80) +); +CREATE INDEX types_name ON types (name); + +CREATE TABLE owners ( + id INTEGER IDENTITY PRIMARY KEY, + first_name VARCHAR(30), + last_name VARCHAR(30), + address VARCHAR(255), + city VARCHAR(80), + telephone VARCHAR(12) +); +CREATE INDEX owners_last_name ON owners (last_name); + +CREATE TABLE pets ( + id INTEGER IDENTITY PRIMARY KEY, + name VARCHAR(30), + birth_date DATE, + type_id INTEGER NOT NULL, + owner_id INTEGER NOT NULL +); +ALTER TABLE pets ADD CONSTRAINT fk_pets_owners FOREIGN KEY (owner_id) REFERENCES owners (id); +ALTER TABLE pets ADD CONSTRAINT fk_pets_types FOREIGN KEY (type_id) REFERENCES types (id); +CREATE INDEX pets_name ON pets (name); diff --git a/src/main/resources/db/mysql/data.sql b/src/main/resources/db/mysql/data.sql new file mode 100644 index 0000000..d6e91b1 --- /dev/null +++ b/src/main/resources/db/mysql/data.sql @@ -0,0 +1,31 @@ +INSERT IGNORE INTO types VALUES (1, 'cat'); +INSERT IGNORE INTO types VALUES (2, 'dog'); +INSERT IGNORE INTO types VALUES (3, 'lizard'); +INSERT IGNORE INTO types VALUES (4, 'snake'); +INSERT IGNORE INTO types VALUES (5, 'bird'); +INSERT IGNORE INTO types VALUES (6, 'hamster'); + +INSERT IGNORE INTO owners VALUES (1, 'George', 'Franklin', '110 W. Liberty St.', 'Madison', '6085551023'); +INSERT IGNORE INTO owners VALUES (2, 'Betty', 'Davis', '638 Cardinal Ave.', 'Sun Prairie', '6085551749'); +INSERT IGNORE INTO owners VALUES (3, 'Eduardo', 'Rodriquez', '2693 Commerce St.', 'McFarland', '6085558763'); +INSERT IGNORE INTO owners VALUES (4, 'Harold', 'Davis', '563 Friendly St.', 'Windsor', '6085553198'); +INSERT IGNORE INTO owners VALUES (5, 'Peter', 'McTavish', '2387 S. Fair Way', 'Madison', '6085552765'); +INSERT IGNORE INTO owners VALUES (6, 'Jean', 'Coleman', '105 N. Lake St.', 'Monona', '6085552654'); +INSERT IGNORE INTO owners VALUES (7, 'Jeff', 'Black', '1450 Oak Blvd.', 'Monona', '6085555387'); +INSERT IGNORE INTO owners VALUES (8, 'Maria', 'Escobito', '345 Maple St.', 'Madison', '6085557683'); +INSERT IGNORE INTO owners VALUES (9, 'David', 'Schroeder', '2749 Blackhawk Trail', 'Madison', '6085559435'); +INSERT IGNORE INTO owners VALUES (10, 'Carlos', 'Estaban', '2335 Independence La.', 'Waunakee', '6085555487'); + +INSERT IGNORE INTO pets VALUES (1, 'Leo', '2000-09-07', 1, 1); +INSERT IGNORE INTO pets VALUES (2, 'Basil', '2002-08-06', 6, 2); +INSERT IGNORE INTO pets VALUES (3, 'Rosy', '2001-04-17', 2, 3); +INSERT IGNORE INTO pets VALUES (4, 'Jewel', '2000-03-07', 2, 3); +INSERT IGNORE INTO pets VALUES (5, 'Iggy', '2000-11-30', 3, 4); +INSERT IGNORE INTO pets VALUES (6, 'George', '2000-01-20', 4, 5); +INSERT IGNORE INTO pets VALUES (7, 'Samantha', '1995-09-04', 1, 6); +INSERT IGNORE INTO pets VALUES (8, 'Max', '1995-09-04', 1, 6); +INSERT IGNORE INTO pets VALUES (9, 'Lucky', '1999-08-06', 5, 7); +INSERT IGNORE INTO pets VALUES (10, 'Mulligan', '1997-02-24', 2, 8); +INSERT IGNORE INTO pets VALUES (11, 'Freddy', '2000-03-09', 5, 9); +INSERT IGNORE INTO pets VALUES (12, 'Lucky', '2000-06-24', 2, 10); +INSERT IGNORE INTO pets VALUES (13, 'Sly', '2002-06-08', 1, 10); diff --git a/src/main/resources/db/mysql/schema.sql b/src/main/resources/db/mysql/schema.sql new file mode 100644 index 0000000..33104a0 --- /dev/null +++ b/src/main/resources/db/mysql/schema.sql @@ -0,0 +1,30 @@ +CREATE DATABASE IF NOT EXISTS petclinic; + +USE petclinic; + +CREATE TABLE IF NOT EXISTS types ( + id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(80), + INDEX(name) +) engine=InnoDB; + +CREATE TABLE IF NOT EXISTS owners ( + id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + first_name VARCHAR(30), + last_name VARCHAR(30), + address VARCHAR(255), + city VARCHAR(80), + telephone VARCHAR(20), + INDEX(last_name) +) engine=InnoDB; + +CREATE TABLE IF NOT EXISTS pets ( + id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(30), + birth_date DATE, + type_id INT(4) UNSIGNED NOT NULL, + owner_id INT(4) UNSIGNED NOT NULL, + INDEX(name), + FOREIGN KEY (owner_id) REFERENCES owners(id), + FOREIGN KEY (type_id) REFERENCES types(id) +) engine=InnoDB; diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..5d03f79 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/test/java/org/springframework/samples/petclinic/customers/web/PetResourceTest.java b/src/test/java/org/springframework/samples/petclinic/customers/web/PetResourceTest.java new file mode 100644 index 0000000..0bd2abb --- /dev/null +++ b/src/test/java/org/springframework/samples/petclinic/customers/web/PetResourceTest.java @@ -0,0 +1,77 @@ +package org.springframework.samples.petclinic.customers.web; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.samples.petclinic.customers.model.Owner; +import org.springframework.samples.petclinic.customers.model.OwnerRepository; +import org.springframework.samples.petclinic.customers.model.Pet; +import org.springframework.samples.petclinic.customers.model.PetRepository; +import org.springframework.samples.petclinic.customers.model.PetType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; + + +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Maciej Szarlinski + */ +@ExtendWith(SpringExtension.class) +@WebMvcTest(PetResource.class) +@ActiveProfiles("test") +class PetResourceTest { + + @Autowired + MockMvc mvc; + + @MockBean + PetRepository petRepository; + + @MockBean + OwnerRepository ownerRepository; + + @Test + void shouldGetAPetInJSonFormat() throws Exception { + + Pet pet = setupPet(); + + given(petRepository.findById(2)).willReturn(Optional.of(pet)); + + + mvc.perform(get("/owners/2/pets/2").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.id").value(2)) + .andExpect(jsonPath("$.name").value("Basil")) + .andExpect(jsonPath("$.type.id").value(6)); + } + + private Pet setupPet() { + Owner owner = new Owner(); + owner.setFirstName("George"); + owner.setLastName("Bush"); + + Pet pet = new Pet(); + + pet.setName("Basil"); + pet.setId(2); + + PetType petType = new PetType(); + petType.setId(6); + pet.setType(petType); + + owner.addPet(pet); + return pet; + } +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 0000000..b9c699b --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,18 @@ +spring: + cloud: + config: + enabled: false + sql: + init: + schema-locations: classpath*:db/hsqldb/schema.sql + data-locations: classpath*:db/hsqldb/data.sql + jpa: + hibernate: + ddl-auto: none + +eureka: + client: + enabled: false + +logging.level.org.springframework: INFO +