From fdeab1dc7f70cf336b4b4e67c283b4fc4489bd2b Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 28 Oct 2025 09:24:41 +0100 Subject: [PATCH] Initial commit --- pom.xml | 123 +++++++++++++++++ .../visits/VisitsServiceApplication.java | 32 +++++ .../petclinic/visits/config/MetricConfig.java | 22 +++ .../samples/petclinic/visits/model/Visit.java | 126 ++++++++++++++++++ .../visits/model/VisitRepository.java | 38 ++++++ .../petclinic/visits/web/VisitResource.java | 82 ++++++++++++ src/main/resources/application.yml | 13 ++ src/main/resources/db/hsqldb/data.sql | 4 + src/main/resources/db/hsqldb/schema.sql | 10 ++ src/main/resources/db/mysql/data.sql | 4 + src/main/resources/db/mysql/schema.sql | 11 ++ src/main/resources/logback-spring.xml | 6 + .../visits/web/VisitResourceTest.java | 61 +++++++++ src/test/resources/application-test.yml | 18 +++ 14 files changed, 550 insertions(+) create mode 100644 pom.xml create mode 100644 src/main/java/org/springframework/samples/petclinic/visits/VisitsServiceApplication.java create mode 100644 src/main/java/org/springframework/samples/petclinic/visits/config/MetricConfig.java create mode 100644 src/main/java/org/springframework/samples/petclinic/visits/model/Visit.java create mode 100644 src/main/java/org/springframework/samples/petclinic/visits/model/VisitRepository.java create mode 100644 src/main/java/org/springframework/samples/petclinic/visits/web/VisitResource.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/db/hsqldb/data.sql create mode 100644 src/main/resources/db/hsqldb/schema.sql create mode 100644 src/main/resources/db/mysql/data.sql create mode 100644 src/main/resources/db/mysql/schema.sql create mode 100644 src/main/resources/logback-spring.xml create mode 100644 src/test/java/org/springframework/samples/petclinic/visits/web/VisitResourceTest.java create mode 100644 src/test/resources/application-test.yml diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..5ad4c6b --- /dev/null +++ b/pom.xml @@ -0,0 +1,123 @@ + + + 4.0.0 + + org.springframework.samples.petclinic.visits + spring-petclinic-visits-service + jar + Spring PetClinic Visits 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-web + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.cloud + spring-cloud-starter-config + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + org.hsqldb + hsqldb + runtime + + + org.jolokia + jolokia-core + + + com.mysql + mysql-connector-j + runtime + + + 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 + + + + + + buildDocker + + + + org.codehaus.mojo + exec-maven-plugin + + + + + + diff --git a/src/main/java/org/springframework/samples/petclinic/visits/VisitsServiceApplication.java b/src/main/java/org/springframework/samples/petclinic/visits/VisitsServiceApplication.java new file mode 100644 index 0000000..78f2963 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/visits/VisitsServiceApplication.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.visits; + +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 VisitsServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(VisitsServiceApplication.class, args); + } +} diff --git a/src/main/java/org/springframework/samples/petclinic/visits/config/MetricConfig.java b/src/main/java/org/springframework/samples/petclinic/visits/config/MetricConfig.java new file mode 100644 index 0000000..35e81f0 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/visits/config/MetricConfig.java @@ -0,0 +1,22 @@ +package org.springframework.samples.petclinic.visits.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/visits/model/Visit.java b/src/main/java/org/springframework/samples/petclinic/visits/model/Visit.java new file mode 100644 index 0000000..ef0fa1a --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/visits/model/Visit.java @@ -0,0 +1,126 @@ +/* + * 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.visits.model; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.*; +import jakarta.validation.constraints.Size; + +import java.util.Date; + +/** + * Simple JavaBean domain object representing a visit. + * + * @author Ken Krebs + * @author Maciej Szarlinski + * @author Ramazan Sakin + */ +@Entity +@Table(name = "visits") +public class Visit { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(name = "visit_date") + @Temporal(TemporalType.TIMESTAMP) + @JsonFormat(pattern = "yyyy-MM-dd") + private Date date = new Date(); + + @Size(max = 8192) + @Column(name = "description") + private String description; + + @Column(name = "pet_id") + private int petId; + + public Integer getId() { + return this.id; + } + + public Date getDate() { + return this.date; + } + + public String getDescription() { + return this.description; + } + + public int getPetId() { + return this.petId; + } + + public void setId(Integer id) { + this.id = id; + } + + public void setDate(Date date) { + this.date = date; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setPetId(int petId) { + this.petId = petId; + } + + + public static final class VisitBuilder { + private Integer id; + private Date date; + private @Size(max = 8192) String description; + private int petId; + + private VisitBuilder() { + } + + public static VisitBuilder aVisit() { + return new VisitBuilder(); + } + + public VisitBuilder id(Integer id) { + this.id = id; + return this; + } + + public VisitBuilder date(Date date) { + this.date = date; + return this; + } + + public VisitBuilder description(String description) { + this.description = description; + return this; + } + + public VisitBuilder petId(int petId) { + this.petId = petId; + return this; + } + + public Visit build() { + Visit visit = new Visit(); + visit.setId(id); + visit.setDate(date); + visit.setDescription(description); + visit.setPetId(petId); + return visit; + } + } +} diff --git a/src/main/java/org/springframework/samples/petclinic/visits/model/VisitRepository.java b/src/main/java/org/springframework/samples/petclinic/visits/model/VisitRepository.java new file mode 100644 index 0000000..bd7a6d7 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/visits/model/VisitRepository.java @@ -0,0 +1,38 @@ +/* + * 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.visits.model; + +import java.util.Collection; +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * Repository class for Visit 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 VisitRepository extends JpaRepository { + + List findByPetId(int petId); + + List findByPetIdIn(Collection petIds); +} diff --git a/src/main/java/org/springframework/samples/petclinic/visits/web/VisitResource.java b/src/main/java/org/springframework/samples/petclinic/visits/web/VisitResource.java new file mode 100644 index 0000000..07f42ba --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/visits/web/VisitResource.java @@ -0,0 +1,82 @@ +/* + * 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.visits.web; + +import java.util.List; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; + +import io.micrometer.core.annotation.Timed; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.samples.petclinic.visits.model.Visit; +import org.springframework.samples.petclinic.visits.model.VisitRepository; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Juergen Hoeller + * @author Ken Krebs + * @author Arjen Poutsma + * @author Michael Isvy + * @author Maciej Szarlinski + * @author Ramazan Sakin + */ +@RestController +@Timed("petclinic.visit") +class VisitResource { + + private static final Logger log = LoggerFactory.getLogger(VisitResource.class); + + private final VisitRepository visitRepository; + + VisitResource(VisitRepository visitRepository) { + this.visitRepository = visitRepository; + } + + @PostMapping("owners/*/pets/{petId}/visits") + @ResponseStatus(HttpStatus.CREATED) + public Visit create( + @Valid @RequestBody Visit visit, + @PathVariable("petId") @Min(1) int petId) { + + visit.setPetId(petId); + log.info("Saving visit {}", visit); + return visitRepository.save(visit); + } + + @GetMapping("owners/*/pets/{petId}/visits") + public List read(@PathVariable("petId") @Min(1) int petId) { + return visitRepository.findByPetId(petId); + } + + @GetMapping("pets/visits") + public Visits read(@RequestParam("petId") List petIds) { + final List byPetIdIn = visitRepository.findByPetIdIn(petIds); + return new Visits(byPetIdIn); + } + + record Visits( + List items + ) { + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..ada0219 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,13 @@ +spring: + application: + name: visits-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..29bc762 --- /dev/null +++ b/src/main/resources/db/hsqldb/data.sql @@ -0,0 +1,4 @@ +INSERT INTO visits VALUES (1, 7, '2013-01-01', 'rabies shot'); +INSERT INTO visits VALUES (2, 8, '2013-01-02', 'rabies shot'); +INSERT INTO visits VALUES (3, 8, '2013-01-03', 'neutered'); +INSERT INTO visits VALUES (4, 7, '2013-01-04', 'spayed'); diff --git a/src/main/resources/db/hsqldb/schema.sql b/src/main/resources/db/hsqldb/schema.sql new file mode 100644 index 0000000..9714a58 --- /dev/null +++ b/src/main/resources/db/hsqldb/schema.sql @@ -0,0 +1,10 @@ +DROP TABLE visits IF EXISTS; + +CREATE TABLE visits ( + id INTEGER IDENTITY PRIMARY KEY, + pet_id INTEGER NOT NULL, + visit_date DATE, + description VARCHAR(8192) +); + +CREATE INDEX visits_pet_id ON visits (pet_id); diff --git a/src/main/resources/db/mysql/data.sql b/src/main/resources/db/mysql/data.sql new file mode 100644 index 0000000..d57dce6 --- /dev/null +++ b/src/main/resources/db/mysql/data.sql @@ -0,0 +1,4 @@ +INSERT IGNORE INTO visits VALUES (1, 7, '2010-03-04', 'rabies shot'); +INSERT IGNORE INTO visits VALUES (2, 8, '2011-03-04', 'rabies shot'); +INSERT IGNORE INTO visits VALUES (3, 8, '2009-06-04', 'neutered'); +INSERT IGNORE INTO visits VALUES (4, 7, '2008-09-04', 'spayed'); diff --git a/src/main/resources/db/mysql/schema.sql b/src/main/resources/db/mysql/schema.sql new file mode 100644 index 0000000..2663f71 --- /dev/null +++ b/src/main/resources/db/mysql/schema.sql @@ -0,0 +1,11 @@ +CREATE DATABASE IF NOT EXISTS petclinic; + +USE petclinic; + +CREATE TABLE IF NOT EXISTS visits ( + id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + pet_id INT(4) UNSIGNED NOT NULL, + visit_date DATE, + description VARCHAR(8192), + FOREIGN KEY (pet_id) REFERENCES pets(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/visits/web/VisitResourceTest.java b/src/test/java/org/springframework/samples/petclinic/visits/web/VisitResourceTest.java new file mode 100644 index 0000000..82392b5 --- /dev/null +++ b/src/test/java/org/springframework/samples/petclinic/visits/web/VisitResourceTest.java @@ -0,0 +1,61 @@ +package org.springframework.samples.petclinic.visits.web; + +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.samples.petclinic.visits.model.Visit; +import org.springframework.samples.petclinic.visits.model.VisitRepository; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; + + +import static java.util.Arrays.asList; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(SpringExtension.class) +@WebMvcTest(VisitResource.class) +@ActiveProfiles("test") +class VisitResourceTest { + + @Autowired + MockMvc mvc; + + @MockBean + VisitRepository visitRepository; + + @Test + void shouldFetchVisits() throws Exception { + given(visitRepository.findByPetIdIn(asList(111, 222))) + .willReturn( + asList( + Visit.VisitBuilder.aVisit() + .id(1) + .petId(111) + .build(), + Visit.VisitBuilder.aVisit() + .id(2) + .petId(222) + .build(), + Visit.VisitBuilder.aVisit() + .id(3) + .petId(222) + .build() + ) + ); + + mvc.perform(get("/pets/visits?petId=111,222")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.items[0].id").value(1)) + .andExpect(jsonPath("$.items[1].id").value(2)) + .andExpect(jsonPath("$.items[2].id").value(3)) + .andExpect(jsonPath("$.items[0].petId").value(111)) + .andExpect(jsonPath("$.items[1].petId").value(222)) + .andExpect(jsonPath("$.items[2].petId").value(222)); + } +} 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 +