Initial commit
222
pom.xml
Normal file
@ -0,0 +1,222 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.springframework.samples.petclinic.api</groupId>
|
||||
<artifactId>spring-petclinic-api-gateway</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<description>Spring PetClinic API Gateway</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.samples</groupId>
|
||||
<artifactId>spring-petclinic-microservices</artifactId>
|
||||
<version>3.4.1</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<webjars-bootstrap.version>5.3.3</webjars-bootstrap.version>
|
||||
<webjars-font-awesome.version>4.7.0</webjars-font-awesome.version>
|
||||
<webjars-angular.version>1.8.3</webjars-angular.version>
|
||||
<webjars-angular-ui-router.version>1.0.30</webjars-angular-ui-router.version>
|
||||
<webjars-marked.version>14.1.2</webjars-marked.version>
|
||||
<squareup-okhttp3.version>4.12.0</squareup-okhttp3.version>
|
||||
|
||||
<libsass-maven-plugin.version>0.2.29</libsass-maven-plugin.version>
|
||||
<docker.image.exposed.port>8081</docker.image.exposed.port>
|
||||
<docker.image.dockerfile.dir>${basedir}/../docker</docker.image.dockerfile.dir>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-cache</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Cloud -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-config</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-gateway</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Third parties -->
|
||||
<dependency>
|
||||
<groupId>org.jolokia</groupId>
|
||||
<artifactId>jolokia-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-registry-prometheus</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-micrometer</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.opentelemetry</groupId>
|
||||
<artifactId>opentelemetry-exporter-zipkin</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-observation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-tracing-bridge-brave</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.zipkin.reporter2</groupId>
|
||||
<artifactId>zipkin-reporter-brave</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Webjars -->
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>angularjs</artifactId>
|
||||
<version>${webjars-angular.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars.npm</groupId>
|
||||
<artifactId>font-awesome</artifactId>
|
||||
<version>${webjars-font-awesome.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>bootstrap</artifactId>
|
||||
<version>${webjars-bootstrap.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>angular-ui-router</artifactId>
|
||||
<version>${webjars-angular-ui-router.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>webjars-locator-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars.npm</groupId>
|
||||
<artifactId>marked</artifactId>
|
||||
<version>${webjars-marked.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Testing -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>mockwebserver</artifactId>
|
||||
<version>${squareup-okhttp3.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>buildDocker</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>css</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>unpack</id>
|
||||
<?m2e execute onConfiguration,onIncremental?>
|
||||
<phase>generate-resources</phase>
|
||||
<goals>
|
||||
<goal>unpack</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<artifactItems>
|
||||
<artifactItem>
|
||||
<groupId>org.webjars.npm</groupId>
|
||||
<artifactId>bootstrap</artifactId>
|
||||
<version>${webjars-bootstrap.version}</version>
|
||||
</artifactItem>
|
||||
</artifactItems>
|
||||
<outputDirectory>${project.build.directory}/webjars</outputDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>com.gitlab.haynes</groupId>
|
||||
<artifactId>libsass-maven-plugin</artifactId>
|
||||
<version>${libsass-maven-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>generate-resources</phase>
|
||||
<?m2e execute onConfiguration,onIncremental?>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<inputPath>${basedir}/src/main/resources/static/scss/</inputPath>
|
||||
<outputPath>${basedir}/src/main/resources/static/css/</outputPath>
|
||||
<includePath>${project.build.directory}/webjars/META-INF/resources/webjars/bootstrap/${webjars-bootstrap.version}/scss/</includePath>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
|
||||
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory;
|
||||
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder;
|
||||
import org.springframework.cloud.client.circuitbreaker.Customizer;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.server.RequestPredicates;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
|
||||
/**
|
||||
* @author Maciej Szarlinski
|
||||
*/
|
||||
@EnableDiscoveryClient
|
||||
@SpringBootApplication
|
||||
public class ApiGatewayApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ApiGatewayApplication.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@LoadBalanced
|
||||
RestTemplate loadBalancedRestTemplate() {
|
||||
return new RestTemplate();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@LoadBalanced
|
||||
public WebClient.Builder loadBalancedWebClientBuilder() {
|
||||
return WebClient.builder();
|
||||
}
|
||||
|
||||
@Value("classpath:/static/index.html")
|
||||
private Resource indexHtml;
|
||||
|
||||
/**
|
||||
* workaround solution for forwarding to index.html
|
||||
* @see <a href="https://github.com/spring-projects/spring-boot/issues/9785">#9785</a>
|
||||
*/
|
||||
@Bean
|
||||
RouterFunction<?> routerFunction() {
|
||||
RouterFunction router = RouterFunctions.resources("/**", new ClassPathResource("static/"))
|
||||
.andRoute(RequestPredicates.GET("/"),
|
||||
request -> ServerResponse.ok().contentType(MediaType.TEXT_HTML).bodyValue(indexHtml));
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default Resilience4j circuit breaker configuration
|
||||
*/
|
||||
@Bean
|
||||
public Customizer<ReactiveResilience4JCircuitBreakerFactory> defaultCustomizer() {
|
||||
return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
|
||||
.circuitBreakerConfig(CircuitBreakerConfig.ofDefaults())
|
||||
.timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(10)).build())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.api.application;
|
||||
|
||||
import org.springframework.samples.petclinic.api.dto.OwnerDetails;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* @author Maciej Szarlinski
|
||||
*/
|
||||
@Component
|
||||
public class CustomersServiceClient {
|
||||
|
||||
private final WebClient.Builder webClientBuilder;
|
||||
|
||||
public CustomersServiceClient(WebClient.Builder webClientBuilder) {
|
||||
this.webClientBuilder = webClientBuilder;
|
||||
}
|
||||
|
||||
public Mono<OwnerDetails> getOwner(final int ownerId) {
|
||||
return webClientBuilder.build().get()
|
||||
.uri("http://customers-service/owners/{ownerId}", ownerId)
|
||||
.retrieve()
|
||||
.bodyToMono(OwnerDetails.class);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.api.application;
|
||||
|
||||
import org.springframework.samples.petclinic.api.dto.Visits;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static java.util.stream.Collectors.joining;
|
||||
|
||||
/**
|
||||
* @author Maciej Szarlinski
|
||||
*/
|
||||
@Component
|
||||
public class VisitsServiceClient {
|
||||
|
||||
// Could be changed for testing purpose
|
||||
private String hostname = "http://visits-service/";
|
||||
|
||||
private final WebClient.Builder webClientBuilder;
|
||||
|
||||
public VisitsServiceClient(WebClient.Builder webClientBuilder) {
|
||||
this.webClientBuilder = webClientBuilder;
|
||||
}
|
||||
|
||||
public Mono<Visits> getVisitsForPets(final List<Integer> petIds) {
|
||||
return webClientBuilder.build()
|
||||
.get()
|
||||
.uri(hostname + "pets/visits?petId={petId}", joinIds(petIds))
|
||||
.retrieve()
|
||||
.bodyToMono(Visits.class);
|
||||
}
|
||||
|
||||
private String joinIds(List<Integer> petIds) {
|
||||
return petIds.stream().map(Object::toString).collect(joining(","));
|
||||
}
|
||||
|
||||
void setHostname(String hostname) {
|
||||
this.hostname = hostname;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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.api.boundary.web;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory;
|
||||
import org.springframework.samples.petclinic.api.application.CustomersServiceClient;
|
||||
import org.springframework.samples.petclinic.api.application.VisitsServiceClient;
|
||||
import org.springframework.samples.petclinic.api.dto.OwnerDetails;
|
||||
import org.springframework.samples.petclinic.api.dto.Visits;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* @author Maciej Szarlinski
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/gateway")
|
||||
public class ApiGatewayController {
|
||||
|
||||
private final CustomersServiceClient customersServiceClient;
|
||||
|
||||
private final VisitsServiceClient visitsServiceClient;
|
||||
|
||||
private final ReactiveCircuitBreakerFactory cbFactory;
|
||||
|
||||
public ApiGatewayController(CustomersServiceClient customersServiceClient,
|
||||
VisitsServiceClient visitsServiceClient,
|
||||
ReactiveCircuitBreakerFactory cbFactory) {
|
||||
this.customersServiceClient = customersServiceClient;
|
||||
this.visitsServiceClient = visitsServiceClient;
|
||||
this.cbFactory = cbFactory;
|
||||
}
|
||||
|
||||
@GetMapping(value = "owners/{ownerId}")
|
||||
public Mono<OwnerDetails> getOwnerDetails(final @PathVariable int ownerId) {
|
||||
return customersServiceClient.getOwner(ownerId)
|
||||
.flatMap(owner ->
|
||||
visitsServiceClient.getVisitsForPets(owner.getPetIds())
|
||||
.transform(it -> {
|
||||
ReactiveCircuitBreaker cb = cbFactory.create("getOwnerDetails");
|
||||
return cb.run(it, throwable -> emptyVisitsForPets());
|
||||
})
|
||||
.map(addVisitsToOwner(owner))
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private Function<Visits, OwnerDetails> addVisitsToOwner(OwnerDetails owner) {
|
||||
return visits -> {
|
||||
owner.pets()
|
||||
.forEach(pet -> pet.visits()
|
||||
.addAll(visits.items().stream()
|
||||
.filter(v -> v.petId() == pet.id())
|
||||
.toList())
|
||||
);
|
||||
return owner;
|
||||
};
|
||||
}
|
||||
|
||||
private Mono<Visits> emptyVisitsForPets() {
|
||||
return Mono.just(new Visits(List.of()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package org.springframework.samples.petclinic.api.boundary.web;
|
||||
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class FallbackController {
|
||||
|
||||
@PostMapping("/fallback")
|
||||
public ResponseEntity<String> fallback() {
|
||||
return ResponseEntity.status(HttpStatus.SC_SERVICE_UNAVAILABLE)
|
||||
.body("Chat is currently unavailable. Please try again later.");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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.api.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Maciej Szarlinski
|
||||
*/
|
||||
public record OwnerDetails(
|
||||
int id,
|
||||
String firstName,
|
||||
String lastName,
|
||||
String address,
|
||||
String city,
|
||||
String telephone,
|
||||
List<PetDetails> pets) {
|
||||
|
||||
@JsonIgnore
|
||||
public List<Integer> getPetIds() {
|
||||
return pets.stream()
|
||||
.map(PetDetails::id)
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
public static final class OwnerDetailsBuilder {
|
||||
private int id;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
private String address;
|
||||
private String city;
|
||||
private String telephone;
|
||||
private List<PetDetails> pets;
|
||||
|
||||
private OwnerDetailsBuilder() {
|
||||
}
|
||||
|
||||
public static OwnerDetailsBuilder anOwnerDetails() {
|
||||
return new OwnerDetailsBuilder();
|
||||
}
|
||||
|
||||
public OwnerDetailsBuilder id(int id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OwnerDetailsBuilder firstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OwnerDetailsBuilder lastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OwnerDetailsBuilder address(String address) {
|
||||
this.address = address;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OwnerDetailsBuilder city(String city) {
|
||||
this.city = city;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OwnerDetailsBuilder telephone(String telephone) {
|
||||
this.telephone = telephone;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OwnerDetailsBuilder pets(List<PetDetails> pets) {
|
||||
this.pets = pets;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OwnerDetails build() {
|
||||
return new OwnerDetails(id, firstName, lastName, address, city, telephone, pets);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.api.dto;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Maciej Szarlinski
|
||||
*/
|
||||
public record PetDetails(
|
||||
int id,
|
||||
String name,
|
||||
String birthDate,
|
||||
PetType type,
|
||||
List<VisitDetails> visits) {
|
||||
|
||||
public PetDetails {
|
||||
if (visits == null) {
|
||||
visits = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
public static final class PetDetailsBuilder {
|
||||
private int id;
|
||||
private String name;
|
||||
private String birthDate;
|
||||
private PetType type;
|
||||
private List<VisitDetails> visits;
|
||||
|
||||
private PetDetailsBuilder() {
|
||||
}
|
||||
|
||||
public static PetDetailsBuilder aPetDetails() {
|
||||
return new PetDetailsBuilder();
|
||||
}
|
||||
|
||||
public PetDetailsBuilder id(int id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PetDetailsBuilder name(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PetDetailsBuilder birthDate(String birthDate) {
|
||||
this.birthDate = birthDate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PetDetailsBuilder type(PetType type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PetDetailsBuilder visits(List<VisitDetails> visits) {
|
||||
this.visits = visits;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PetDetails build() {
|
||||
return new PetDetails(id, name, birthDate, type, visits);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.api.dto;
|
||||
|
||||
/**
|
||||
* @author Maciej Szarlinski
|
||||
*/
|
||||
public record PetType(String name) {
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.api.dto;
|
||||
|
||||
/**
|
||||
* @author Maciej Szarlinski
|
||||
*/
|
||||
public record VisitDetails (
|
||||
Integer id,
|
||||
Integer petId,
|
||||
String date,
|
||||
String description) {
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.api.dto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* @author Maciej Szarlinski
|
||||
*/
|
||||
public record Visits (
|
||||
List<VisitDetails> items
|
||||
) {
|
||||
public Visits() {
|
||||
this(new ArrayList<>());
|
||||
}
|
||||
}
|
||||
50
src/main/resources/application.yml
Normal file
@ -0,0 +1,50 @@
|
||||
spring:
|
||||
application:
|
||||
name: api-gateway
|
||||
config:
|
||||
import: optional:configserver:${CONFIG_SERVER_URL:http://localhost:8888/}
|
||||
cloud:
|
||||
gateway:
|
||||
default-filters:
|
||||
- name: CircuitBreaker
|
||||
args:
|
||||
name: defaultCircuitBreaker
|
||||
fallbackUri: forward:/fallback
|
||||
- name: Retry
|
||||
args:
|
||||
retries: 1
|
||||
statuses: SERVICE_UNAVAILABLE
|
||||
methods: POST
|
||||
routes:
|
||||
- id: vets-service
|
||||
uri: lb://vets-service
|
||||
predicates:
|
||||
- Path=/api/vet/**
|
||||
filters:
|
||||
- StripPrefix=2
|
||||
- id: visits-service
|
||||
uri: lb://visits-service
|
||||
predicates:
|
||||
- Path=/api/visit/**
|
||||
filters:
|
||||
- StripPrefix=2
|
||||
- id: customers-service
|
||||
uri: lb://customers-service
|
||||
predicates:
|
||||
- Path=/api/customer/**
|
||||
filters:
|
||||
- StripPrefix=2
|
||||
- id: genai-service
|
||||
uri: lb://genai-service
|
||||
predicates:
|
||||
- Path=/api/genai/**
|
||||
filters:
|
||||
- StripPrefix=2
|
||||
- CircuitBreaker=name=genaiCircuitBreaker,fallbackUri=/fallback
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: docker
|
||||
import: configserver:http://config-server:8888
|
||||
6
src/main/resources/logback-spring.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<include resource="org/springframework/boot/logging/logback/base.xml"/>
|
||||
<!-- Required for Loglevel managment into the Spring Petclinic Admin Server-->
|
||||
<jmxConfigurator/>
|
||||
</configuration>
|
||||
7
src/main/resources/messages/messages.properties
Normal file
@ -0,0 +1,7 @@
|
||||
required=is required
|
||||
notFound=has not been found
|
||||
duplicate=is already in use
|
||||
nonNumeric=must be all numeric
|
||||
duplicateFormSubmission=Duplicate form submission is not allowed
|
||||
typeMismatch.date=invalid date
|
||||
typeMismatch.birthDate=invalid date
|
||||
7
src/main/resources/messages/messages_de.properties
Normal file
@ -0,0 +1,7 @@
|
||||
required=muss angegeben werden
|
||||
notFound=wurde nicht gefunden
|
||||
duplicate=ist bereits vergeben
|
||||
nonNumeric=darf nur numerisch sein
|
||||
duplicateFormSubmission=Wiederholtes Absenden des Formulars ist nicht erlaubt
|
||||
typeMismatch.date=ung<EFBFBD>ltiges Datum
|
||||
typeMismatch.birthDate=ung<EFBFBD>ltiges Datum
|
||||
1
src/main/resources/messages/messages_en.properties
Normal file
@ -0,0 +1 @@
|
||||
# This file is intentionally empty. Message look-ups will fall back to the default "messages.properties" file.
|
||||
62
src/main/resources/static/css/header.css
Normal file
@ -0,0 +1,62 @@
|
||||
.navbar {
|
||||
border-top: 4px solid #6db33f;
|
||||
background-color: #34302d;
|
||||
margin-bottom: 0px;
|
||||
border-bottom: 0;
|
||||
border-left: 0;
|
||||
border-right: 0; }
|
||||
|
||||
.navbar a.navbar-brand {
|
||||
background: url("../images/spring-logo-dataflow.png") -1px -1px no-repeat;
|
||||
margin: 12px 0 6px;
|
||||
width: 229px;
|
||||
height: 46px;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
padding: 0; }
|
||||
|
||||
.navbar a.navbar-brand span {
|
||||
display: block;
|
||||
width: 229px;
|
||||
height: 46px;
|
||||
background: url("../images/spring-logo-dataflow.png") -1px -48px no-repeat;
|
||||
opacity: 0;
|
||||
-moz-transition: opacity 0.12s ease-in-out;
|
||||
-webkit-transition: opacity 0.12s ease-in-out;
|
||||
-o-transition: opacity 0.12s ease-in-out; }
|
||||
|
||||
.navbar a.navbar-brand:hover span {
|
||||
opacity: 1; }
|
||||
|
||||
.navbar li > a, .navbar-text {
|
||||
font-family: "montserratregular", sans-serif;
|
||||
text-shadow: none;
|
||||
font-size: 14px;
|
||||
/* line-height: 14px; */
|
||||
padding: 28px 20px;
|
||||
transition: all 0.15s;
|
||||
-webkit-transition: all 0.15s;
|
||||
-moz-transition: all 0.15s;
|
||||
-o-transition: all 0.15s;
|
||||
-ms-transition: all 0.15s; }
|
||||
|
||||
.navbar li > a {
|
||||
text-transform: uppercase; }
|
||||
|
||||
.navbar .navbar-text {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0; }
|
||||
|
||||
.navbar li:hover > a {
|
||||
color: #eeeeee;
|
||||
background-color: #6db33f; }
|
||||
|
||||
.navbar-toggle {
|
||||
border-width: 0; }
|
||||
.navbar-toggle .icon-bar + .icon-bar {
|
||||
margin-top: 3px; }
|
||||
.navbar-toggle .icon-bar {
|
||||
width: 19px;
|
||||
height: 3px; }
|
||||
|
||||
/*# sourceMappingURL=../../../../../target/header.css.map */
|
||||
9613
src/main/resources/static/css/petclinic.css
Normal file
28
src/main/resources/static/css/responsive.css
Normal file
@ -0,0 +1,28 @@
|
||||
@media (max-width: 768px) {
|
||||
.navbar-toggle {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
left: 0px;
|
||||
top: 0px; }
|
||||
.navbar a.navbar-brand {
|
||||
display: block;
|
||||
margin: 0 auto 0 auto;
|
||||
width: 148px;
|
||||
height: 50px;
|
||||
float: none;
|
||||
background: url("../images/spring-logo-dataflow-mobile.png") 0 center no-repeat; }
|
||||
.homepage-billboard .homepage-subtitle {
|
||||
font-size: 21px;
|
||||
line-height: 21px; }
|
||||
.navbar a.navbar-brand span {
|
||||
display: none; }
|
||||
.navbar {
|
||||
border-top-width: 0; }
|
||||
.xd-container {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 30px; }
|
||||
.index-page--subtitle {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 30px; } }
|
||||
|
||||
/*# sourceMappingURL=../../../../../target/responsive.css.map */
|
||||
43
src/main/resources/static/css/typography.css
Normal file
@ -0,0 +1,43 @@
|
||||
@font-face {
|
||||
font-family: 'varela_roundregular';
|
||||
src: url("../fonts/varela_round-webfont.eot");
|
||||
src: url("../fonts/varela_round-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/varela_round-webfont.woff") format("woff"), url("../fonts/varela_round-webfont.ttf") format("truetype"), url("../fonts/varela_round-webfont.svg#varela_roundregular") format("svg");
|
||||
font-weight: normal;
|
||||
font-style: normal; }
|
||||
|
||||
@font-face {
|
||||
font-family: 'montserratregular';
|
||||
src: url("../fonts/montserrat-webfont.eot");
|
||||
src: url("../fonts/montserrat-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/montserrat-webfont.woff") format("woff"), url("../fonts/montserrat-webfont.ttf") format("truetype"), url("../fonts/montserrat-webfont.svg#montserratregular") format("svg");
|
||||
font-weight: normal;
|
||||
font-style: normal; }
|
||||
|
||||
body, h1, h2, h3, p, input {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
font-family: "varela_roundregular", sans-serif;
|
||||
color: #34302d; }
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
line-height: 30px;
|
||||
font-family: "montserratregular", sans-serif; }
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
margin-bottom: 10px;
|
||||
font-family: "montserratregular", sans-serif; }
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 700; }
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
font-family: "montserratregular", sans-serif; }
|
||||
|
||||
/*# sourceMappingURL=../../../../../target/typography.css.map */
|
||||
BIN
src/main/resources/static/fonts/montserrat-webfont.eot
Normal file
1283
src/main/resources/static/fonts/montserrat-webfont.svg
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
src/main/resources/static/fonts/montserrat-webfont.ttf
Normal file
BIN
src/main/resources/static/fonts/montserrat-webfont.woff
Normal file
BIN
src/main/resources/static/fonts/varela_round-webfont.eot
Normal file
7875
src/main/resources/static/fonts/varela_round-webfont.svg
Normal file
|
After Width: | Height: | Size: 362 KiB |
BIN
src/main/resources/static/fonts/varela_round-webfont.ttf
Normal file
BIN
src/main/resources/static/fonts/varela_round-webfont.woff
Normal file
BIN
src/main/resources/static/images/favicon.png
Normal file
|
After Width: | Height: | Size: 528 B |
BIN
src/main/resources/static/images/pets.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
src/main/resources/static/images/platform-bg.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
src/main/resources/static/images/spring-logo-dataflow-mobile.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src/main/resources/static/images/spring-logo-dataflow.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
src/main/resources/static/images/spring-pivotal-logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
95
src/main/resources/static/index.html
Normal file
@ -0,0 +1,95 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html ng-app="petClinicApp" lang="en">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<meta charset="utf-8"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0, minimal-ui"/>
|
||||
<!-- The above 4 meta tags *must* come first in the head; any other head content must come *after* these tags -->
|
||||
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/images/favicon.png"/>
|
||||
|
||||
<title>PetClinic :: a Spring Framework demonstration</title>
|
||||
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="/css/petclinic.css"/>
|
||||
<link rel="stylesheet" href="/webjars/font-awesome/css/font-awesome.min.css">
|
||||
|
||||
<script src="/webjars/bootstrap/js/bootstrap.min.js"></script>
|
||||
|
||||
<script src="/webjars/angularjs/angular.min.js"></script>
|
||||
<script src="/webjars/angular-ui-router/angular-ui-router.min.js"></script>
|
||||
|
||||
<script src="/scripts/app.js"></script>
|
||||
<script src="/scripts/genai/chat.js"></script>
|
||||
|
||||
<script src="/scripts/owner-list/owner-list.js"></script>
|
||||
<script src="/scripts/owner-list/owner-list.controller.js"></script>
|
||||
<script src="/scripts/owner-list/owner-list.component.js"></script>
|
||||
|
||||
<script src="/scripts/owner-details/owner-details.js"></script>
|
||||
<script src="/scripts/owner-details/owner-details.controller.js"></script>
|
||||
<script src="/scripts/owner-details/owner-details.component.js"></script>
|
||||
|
||||
<script src="/scripts/owner-form/owner-form.js"></script>
|
||||
<script src="/scripts/owner-form/owner-form.controller.js"></script>
|
||||
<script src="/scripts/owner-form/owner-form.component.js"></script>
|
||||
|
||||
<script src="/scripts/pet-form/pet-form.js"></script>
|
||||
<script src="/scripts/pet-form/pet-form.controller.js"></script>
|
||||
<script src="/scripts/pet-form/pet-form.component.js"></script>
|
||||
|
||||
<script src="/scripts/visits/visits.js"></script>
|
||||
<script src="/scripts/visits/visits.controller.js"></script>
|
||||
<script src="/scripts/visits/visits.component.js"></script>
|
||||
|
||||
<script src="/scripts/vet-list/vet-list.js"></script>
|
||||
<script src="/scripts/vet-list/vet-list.controller.js"></script>
|
||||
<script src="/scripts/vet-list/vet-list.component.js"></script>
|
||||
|
||||
<script src="/scripts/infrastructure/infrastructure.js"></script>
|
||||
<script src="/scripts/infrastructure/httpErrorHandlingInterceptor.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<layout-nav></layout-nav>
|
||||
<div class="container-fluid">
|
||||
<div class="container xd-container">
|
||||
<div ui-view=""></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chatbox" id="chatbox">
|
||||
<button class="chatbox-header" onclick="toggleChatbox()">
|
||||
Chat with Us!
|
||||
</button>
|
||||
<div class="chatbox-content" id="chatbox-content">
|
||||
<div class="chatbox-messages" id="chatbox-messages">
|
||||
<!-- Chat messages will be dynamically inserted here -->
|
||||
</div>
|
||||
<div class="chatbox-footer">
|
||||
<input type="text" id="chatbox-input" placeholder="Type a message..." onkeydown="handleKeyPress(event)" />
|
||||
<button onclick="sendMessage()">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript for handling chatbox interaction -->
|
||||
<script>
|
||||
|
||||
// Call loadChatMessages when the page loads
|
||||
window.onload = loadChatMessages;
|
||||
|
||||
// Ensure messages are saved when navigating away
|
||||
window.onbeforeunload = saveChatMessages;
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<script src="/webjars/marked/marked.min.js"></script>
|
||||
|
||||
<layout-footer></layout-footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
36
src/main/resources/static/scripts/app.js
Normal file
@ -0,0 +1,36 @@
|
||||
'use strict';
|
||||
/* App Module */
|
||||
var petClinicApp = angular.module('petClinicApp', [
|
||||
'ui.router', 'infrastructure', 'layoutNav', 'layoutFooter', 'layoutWelcome',
|
||||
'ownerList', 'ownerDetails', 'ownerForm', 'petForm', 'visits', 'vetList']);
|
||||
|
||||
petClinicApp.config(['$stateProvider', '$urlRouterProvider', '$locationProvider', '$httpProvider', function(
|
||||
$stateProvider, $urlRouterProvider, $locationProvider, $httpProvider) {
|
||||
|
||||
// safari turns to be lazy sending the Cache-Control header
|
||||
$httpProvider.defaults.headers.common["Cache-Control"] = 'no-cache';
|
||||
$httpProvider.interceptors.push('HttpErrorHandlingInterceptor');
|
||||
|
||||
$locationProvider.hashPrefix('!');
|
||||
|
||||
$urlRouterProvider.otherwise('/welcome');
|
||||
$stateProvider
|
||||
.state('app', {
|
||||
abstract: true,
|
||||
url: '',
|
||||
template: '<ui-view></ui-view>'
|
||||
})
|
||||
.state('welcome', {
|
||||
parent: 'app',
|
||||
url: '/welcome',
|
||||
template: '<layout-welcome></layout-welcome>'
|
||||
});
|
||||
}]);
|
||||
|
||||
['welcome', 'nav', 'footer'].forEach(function(c) {
|
||||
var mod = 'layout' + c.toUpperCase().substring(0, 1) + c.substring(1);
|
||||
angular.module(mod, []);
|
||||
angular.module(mod).component(mod, {
|
||||
templateUrl: "scripts/fragments/" + c + ".html"
|
||||
});
|
||||
});
|
||||
6
src/main/resources/static/scripts/fragments/footer.html
Normal file
@ -0,0 +1,6 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 text-center"><img src="/images/spring-pivotal-logo.png"
|
||||
alt="Sponsored by Pivotal"/></div>
|
||||
</div>
|
||||
</div>
|
||||
44
src/main/resources/static/scripts/fragments/nav.html
Normal file
@ -0,0 +1,44 @@
|
||||
<nav class="navbar navbar-expand-lg navbar-dark" role="navigation">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/"><span></span></a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#main-navbar">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="main-navbar" style="">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" ui-sref-active="active" href="/" title="home page">
|
||||
<span class="fa fa-home"></span>
|
||||
<span>Home</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="nav-link" ui-sref-active="active" ui-sref="owners" title="find owners">
|
||||
<span class="fa fa-search"></span>
|
||||
<span>Find owners</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="nav-link" ui-sref-active="active" ui-sref="ownerNew" title="register owner">
|
||||
<span class="fa fa-plus"></span>
|
||||
<span>Register owner</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="nav-link" ui-sref-active="active" ui-sref="vets" title="veterinarians">
|
||||
<span class="fa fa-th-list"></span>
|
||||
<span>Veterinarians</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
|
||||
7
src/main/resources/static/scripts/fragments/welcome.html
Normal file
@ -0,0 +1,7 @@
|
||||
<h1>Welcome to Petclinic</h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<img class="img-responsive" src="images/pets.png" alt="pets logo" />
|
||||
</div>
|
||||
</div>
|
||||
81
src/main/resources/static/scripts/genai/chat.js
Normal file
@ -0,0 +1,81 @@
|
||||
function appendMessage(message, type) {
|
||||
const chatMessages = document.getElementById('chatbox-messages');
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.classList.add('chat-bubble', type);
|
||||
|
||||
// Convert Markdown to HTML
|
||||
const htmlContent = marked.parse(message); // Use marked.parse() for newer versions
|
||||
messageElement.innerHTML = htmlContent;
|
||||
|
||||
chatMessages.appendChild(messageElement);
|
||||
|
||||
// Scroll to the bottom of the chatbox to show the latest message
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
}
|
||||
|
||||
function toggleChatbox() {
|
||||
const chatbox = document.getElementById('chatbox');
|
||||
const chatboxContent = document.getElementById('chatbox-content');
|
||||
|
||||
if (chatbox.classList.contains('minimized')) {
|
||||
chatbox.classList.remove('minimized');
|
||||
chatboxContent.style.height = '400px'; // Set to initial height when expanded
|
||||
} else {
|
||||
chatbox.classList.add('minimized');
|
||||
chatboxContent.style.height = '40px'; // Set to minimized height
|
||||
}
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
const query = document.getElementById('chatbox-input').value;
|
||||
|
||||
// Only send if there's a message
|
||||
if (!query.trim()) return;
|
||||
|
||||
// Clear the input field after sending the message
|
||||
document.getElementById('chatbox-input').value = '';
|
||||
|
||||
// Display user message in the chatbox
|
||||
appendMessage(query, 'user');
|
||||
|
||||
// Send the message to the backend
|
||||
fetch('/api/genai/chatclient', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(query),
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(responseText => {
|
||||
// Display the response from the server in the chatbox
|
||||
appendMessage(responseText, 'bot');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
// Display the fallback message in the chatbox
|
||||
appendMessage('Chat is currently unavailable', 'bot');
|
||||
});
|
||||
}
|
||||
|
||||
function handleKeyPress(event) {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault(); // Prevents adding a newline
|
||||
sendMessage(); // Send the message when Enter is pressed
|
||||
}
|
||||
}
|
||||
|
||||
// Save chat messages to localStorage
|
||||
function saveChatMessages() {
|
||||
const messages = document.getElementById('chatbox-messages').innerHTML;
|
||||
localStorage.setItem('chatMessages', messages);
|
||||
}
|
||||
|
||||
// Load chat messages from localStorage
|
||||
function loadChatMessages() {
|
||||
const messages = localStorage.getItem('chatMessages');
|
||||
if (messages) {
|
||||
document.getElementById('chatbox-messages').innerHTML = messages;
|
||||
document.getElementById('chatbox-messages').scrollTop = document.getElementById('chatbox-messages').scrollHeight;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Global HTTP errors handler.
|
||||
*/
|
||||
angular.module('infrastructure')
|
||||
.factory('HttpErrorHandlingInterceptor', function () {
|
||||
return {
|
||||
responseError: function (response) {
|
||||
var error = response.data;
|
||||
alert(error.error + "\r\n" + error.errors.map(function (e) {
|
||||
return e.field + ": " + e.defaultMessage;
|
||||
}).join("\r\n"));
|
||||
return response;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('infrastructure', []);
|
||||
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('ownerDetails')
|
||||
.component('ownerDetails', {
|
||||
templateUrl: 'scripts/owner-details/owner-details.template.html',
|
||||
controller: 'OwnerDetailsController'
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('ownerDetails')
|
||||
.controller('OwnerDetailsController', ['$http', '$stateParams', function ($http, $stateParams) {
|
||||
var self = this;
|
||||
|
||||
$http.get('api/gateway/owners/' + $stateParams.ownerId).then(function (resp) {
|
||||
self.owner = resp.data;
|
||||
});
|
||||
}]);
|
||||
@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('ownerDetails', ['ui.router'])
|
||||
.config(['$stateProvider', function ($stateProvider) {
|
||||
$stateProvider
|
||||
.state('ownerDetails', {
|
||||
parent: 'app',
|
||||
url: '/owners/details/:ownerId',
|
||||
template: '<owner-details></owner-details>'
|
||||
})
|
||||
}]);
|
||||
@ -0,0 +1,70 @@
|
||||
<h2>Owner Information</h2>
|
||||
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th class="col-sm-3">Name</th>
|
||||
<td><b>{{$ctrl.owner.firstName}} {{$ctrl.owner.lastName}}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Address</th>
|
||||
<td>{{$ctrl.owner.address}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>City</th>
|
||||
<td>{{$ctrl.owner.city}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Telephone</th>
|
||||
<td>{{$ctrl.owner.telephone}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a class="btn btn-primary" ui-sref="ownerEdit({ownerId: $ctrl.owner.id})">Edit Owner</a>
|
||||
</td>
|
||||
<td>
|
||||
<a ui-sref="petNew({ownerId: $ctrl.owner.id})" class="btn btn-primary">Add New Pet</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>Pets and Visits</h2>
|
||||
|
||||
<table class="table table-striped">
|
||||
<tr ng-repeat="pet in $ctrl.owner.pets track by pet.id">
|
||||
<td valign="top">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Name</dt>
|
||||
<dd><a ui-sref="petEdit({ownerId: $ctrl.owner.id, petId: pet.id})">{{pet.name}}</a></dd>
|
||||
<dt>Birth Date</dt>
|
||||
<dd>{{pet.birthDate | date:'yyyy MMM dd'}}</dd>
|
||||
<dt>Type</dt>
|
||||
<dd>{{pet.type.name}}</dd>
|
||||
</dl>
|
||||
</td>
|
||||
<td valign="top">
|
||||
<table class="table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Visit Date</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr ng-repeat="visit in pet.visits track by visit.id">
|
||||
<td>{{visit.date | date:'yyyy MMM dd'}}</td>
|
||||
<td>{{visit.description}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a ui-sref="petEdit({ownerId: $ctrl.owner.id, petId: pet.id})">Edit Pet</a>
|
||||
</td>
|
||||
<td>
|
||||
<a ui-sref="visits({ownerId: $ctrl.owner.id, petId: pet.id})">Add Visit</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('ownerForm')
|
||||
.component('ownerForm', {
|
||||
templateUrl: 'scripts/owner-form/owner-form.template.html',
|
||||
controller: 'OwnerFormController'
|
||||
});
|
||||
@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('ownerForm')
|
||||
.controller('OwnerFormController', ["$http", '$state', '$stateParams', function ($http, $state, $stateParams) {
|
||||
var self = this;
|
||||
|
||||
var ownerId = $stateParams.ownerId || 0;
|
||||
|
||||
if (!ownerId) {
|
||||
self.owner = {};
|
||||
} else {
|
||||
$http.get("api/customer/owners/" + ownerId).then(function (resp) {
|
||||
self.owner = resp.data;
|
||||
});
|
||||
}
|
||||
|
||||
self.submitOwnerForm = function () {
|
||||
var id = self.owner.id;
|
||||
|
||||
if (id) {
|
||||
$http.put('api/customer/owners/' + id, self.owner).then(function () {
|
||||
$state.go('ownerDetails', {ownerId: ownerId});
|
||||
});
|
||||
} else {
|
||||
$http.post('api/customer/owners', self.owner).then(function () {
|
||||
$state.go('owners');
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
16
src/main/resources/static/scripts/owner-form/owner-form.js
Normal file
@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('ownerForm', ['ui.router'])
|
||||
.config(['$stateProvider', function ($stateProvider) {
|
||||
$stateProvider
|
||||
.state('ownerNew', {
|
||||
parent: 'app',
|
||||
url: '/owners/new',
|
||||
template: '<owner-form></owner-form>'
|
||||
})
|
||||
.state('ownerEdit', {
|
||||
parent: 'app',
|
||||
url: '/owners/:ownerId/edit',
|
||||
template: '<owner-form></owner-form>'
|
||||
})
|
||||
}]);
|
||||
@ -0,0 +1,39 @@
|
||||
<h2>Owner</h2>
|
||||
<form ng-submit="$ctrl.submitOwnerForm()" style="max-width: 25em;">
|
||||
<div class="form-group">
|
||||
<label>First name</label>
|
||||
<input class="form-control" ng-model="$ctrl.owner.firstName" name="firstName" required/>
|
||||
<span ng-show="ownerForm.firstName.$error.required" class="help-block">First name is required.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Last name</label>
|
||||
<input class="form-control" ng-model="$ctrl.owner.lastName" name="lastName" required/>
|
||||
<span ng-show="ownerForm.lastName.$error.required" class="help-block">Last name is required.</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label>Address</label>
|
||||
<input class="form-control" ng-model="$ctrl.owner.address" name="address" required/>
|
||||
<span ng-show="ownerForm.address.$error.required" class="help-block">Address is required.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>City</label>
|
||||
<input class="form-control" ng-model="$ctrl.owner.city" name="city" required/>
|
||||
<span ng-show="ownerForm.city.$error.required" class="help-block">City is required.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Telephone</label>
|
||||
<input class="form-control" ng-model="$ctrl.owner.telephone" pattern="[0-9]{12}" placeholder="905554443322"
|
||||
name="telephone" maxlength="12" required/>
|
||||
<span ng-show="ownerForm.telephone.$error.required" class="help-block">Telephone is required.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" type="submit">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('ownerList')
|
||||
.component('ownerList', {
|
||||
templateUrl: 'scripts/owner-list/owner-list.template.html',
|
||||
controller: 'OwnerListController'
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('ownerList')
|
||||
.controller('OwnerListController', ['$http', function ($http) {
|
||||
var self = this;
|
||||
|
||||
$http.get('api/customer/owners').then(function (resp) {
|
||||
self.owners = resp.data;
|
||||
});
|
||||
}]);
|
||||
11
src/main/resources/static/scripts/owner-list/owner-list.js
Normal file
@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('ownerList', ['ui.router'])
|
||||
.config(['$stateProvider', function ($stateProvider) {
|
||||
$stateProvider
|
||||
.state('owners', {
|
||||
parent: 'app',
|
||||
url: '/owners',
|
||||
template: '<owner-list></owner-list>'
|
||||
})
|
||||
}]);
|
||||
@ -0,0 +1,31 @@
|
||||
<h2>Owners</h2>
|
||||
|
||||
<form onsubmit="javascript:void(0)" style="max-width: 20em; margin-top: 2em;">
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" placeholder="Search Filter" ng-model="$ctrl.query" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th class="hidden-sm hidden-xs">Address</th>
|
||||
<th>City</th>
|
||||
<th>Telephone</th>
|
||||
<th class="hidden-xs">Pets</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tr ng-repeat="owner in $ctrl.owners | filter:$ctrl.query track by owner.id">
|
||||
<td>
|
||||
<a ui-sref="ownerDetails({ ownerId: owner.id })">
|
||||
{{owner.firstName}} {{owner.lastName}}
|
||||
</a>
|
||||
</td>
|
||||
<td class="hidden-sm hidden-xs">{{owner.address}}</td>
|
||||
<td>{{owner.city}}</td>
|
||||
<td>{{owner.telephone}}</td>
|
||||
<td class="hidden-xs"><span ng-repeat="pet in owner.pets track by pet.id">{{pet.name + ' '}}</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('petForm')
|
||||
.component('petForm', {
|
||||
templateUrl: 'scripts/pet-form/pet-form.template.html',
|
||||
controller: 'PetFormController'
|
||||
});
|
||||
@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('petForm')
|
||||
.controller('PetFormController', ['$http', '$state', '$stateParams', function ($http, $state, $stateParams) {
|
||||
var self = this;
|
||||
var ownerId = $stateParams.ownerId || 0;
|
||||
|
||||
$http.get('api/customer/petTypes').then(function (resp) {
|
||||
self.types = resp.data;
|
||||
}).then(function () {
|
||||
|
||||
var petId = $stateParams.petId || 0;
|
||||
|
||||
if (petId) { // edit
|
||||
$http.get("api/customer/owners/" + ownerId + "/pets/" + petId).then(function (resp) {
|
||||
self.pet = resp.data;
|
||||
self.pet.birthDate = new Date(self.pet.birthDate);
|
||||
self.petTypeId = "" + self.pet.type.id;
|
||||
});
|
||||
} else {
|
||||
$http.get('api/customer/owners/' + ownerId).then(function (resp) {
|
||||
self.pet = {
|
||||
owner: resp.data.firstName + " " + resp.data.lastName
|
||||
};
|
||||
self.petTypeId = "1";
|
||||
})
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
self.submit = function () {
|
||||
var id = self.pet.id || 0;
|
||||
|
||||
var data = {
|
||||
id: id,
|
||||
name: self.pet.name,
|
||||
birthDate: self.pet.birthDate,
|
||||
typeId: self.petTypeId
|
||||
};
|
||||
|
||||
var req;
|
||||
if (id) {
|
||||
req = $http.put("api/customer/owners/" + ownerId + "/pets/" + id, data);
|
||||
} else {
|
||||
req = $http.post("api/customer/owners/" + ownerId + "/pets", data);
|
||||
}
|
||||
|
||||
req.then(function () {
|
||||
$state.go('ownerDetails', {ownerId: ownerId});
|
||||
});
|
||||
};
|
||||
}]);
|
||||
16
src/main/resources/static/scripts/pet-form/pet-form.js
Normal file
@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('petForm', ['ui.router'])
|
||||
.config(['$stateProvider', function ($stateProvider) {
|
||||
$stateProvider
|
||||
.state('petNew', {
|
||||
parent: 'app',
|
||||
url: '/owners/:ownerId/new-pet',
|
||||
template: '<pet-form></pet-form>'
|
||||
})
|
||||
.state('petEdit', {
|
||||
parent: 'app',
|
||||
url: '/owners/:ownerId/pets/:petId',
|
||||
template: '<pet-form></pet-form>'
|
||||
})
|
||||
}]);
|
||||
@ -0,0 +1,43 @@
|
||||
<h2>Pet</h2>
|
||||
|
||||
<form class="form-horizontal" ng-submit="$ctrl.submit()">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">Owner</label>
|
||||
<div class="col-sm-6">
|
||||
<p class="form-control-static">{{$ctrl.pet.owner}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">Name </label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control col-sm-4" ng-model="$ctrl.pet.name" name="name" required type="text"/>
|
||||
<span ng-show="petForm.name.$error.required" class="help-inline">Name is required.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">Birth date</label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" ng-model="$ctrl.pet.birthDate" required type="date"/>
|
||||
<span ng-show="petForm.name.$error.required" class="help-inline"> birth date is required.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">Type</label>
|
||||
<div class="col-sm-6">
|
||||
<select class="form-control" ng-model="$ctrl.petTypeId">
|
||||
<option ng-repeat="t in $ctrl.types track by t.id" value="{{t.id}}">{{t.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-6 col-sm-offset-2">
|
||||
<button class="btn btn-primary" type="submit">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('vetList')
|
||||
.component('vetList', {
|
||||
templateUrl: 'scripts/vet-list/vet-list.template.html',
|
||||
controller: 'VetListController'
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('vetList')
|
||||
.controller('VetListController', ['$http', function ($http) {
|
||||
var self = this;
|
||||
|
||||
$http.get('api/vet/vets').then(function (resp) {
|
||||
self.vetList = resp.data;
|
||||
});
|
||||
}]);
|
||||
11
src/main/resources/static/scripts/vet-list/vet-list.js
Normal file
@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('vetList', ['ui.router'])
|
||||
.config(['$stateProvider', function ($stateProvider) {
|
||||
$stateProvider
|
||||
.state('vets', {
|
||||
parent: 'app',
|
||||
url: '/vets',
|
||||
template: '<vet-list></vet-list>'
|
||||
})
|
||||
}]);
|
||||
@ -0,0 +1,15 @@
|
||||
<h2>Veterinarians</h2>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Specialties</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tr ng-repeat="vet in $ctrl.vetList">
|
||||
<td>{{vet.firstName}} {{vet.lastName}}</td>
|
||||
<td><span ng-repeat="specialty in vet.specialties">{{specialty.name + ' '}}</span></td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('visits')
|
||||
.component('visits', {
|
||||
templateUrl: 'scripts/visits/visits.template.html',
|
||||
controller: 'VisitsController'
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('visits')
|
||||
.controller('VisitsController', ['$http', '$state', '$stateParams', '$filter', function ($http, $state, $stateParams, $filter) {
|
||||
var self = this;
|
||||
var petId = $stateParams.petId || 0;
|
||||
var url = "api/visit/owners/" + ($stateParams.ownerId || 0) + "/pets/" + petId + "/visits";
|
||||
self.date = new Date();
|
||||
self.desc = "";
|
||||
|
||||
$http.get(url).then(function (resp) {
|
||||
self.visits = resp.data;
|
||||
});
|
||||
|
||||
self.submit = function () {
|
||||
var data = {
|
||||
date: $filter('date')(self.date, "yyyy-MM-dd"),
|
||||
description: self.desc
|
||||
};
|
||||
|
||||
$http.post(url, data).then(function () {
|
||||
$state.go('ownerDetails', { ownerId: $stateParams.ownerId });
|
||||
});
|
||||
};
|
||||
}]);
|
||||
11
src/main/resources/static/scripts/visits/visits.js
Normal file
@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('visits', ['ui.router'])
|
||||
.config(['$stateProvider', function ($stateProvider) {
|
||||
$stateProvider
|
||||
.state('visits', {
|
||||
parent: 'app',
|
||||
url: '/owners/:ownerId/pets/:petId/visits',
|
||||
template: '<visits></visits>'
|
||||
})
|
||||
}]);
|
||||
@ -0,0 +1,27 @@
|
||||
<h2>Visits</h2>
|
||||
|
||||
<form ng-submit="$ctrl.submit()">
|
||||
|
||||
<div class="form-group">
|
||||
<label>Date</label>
|
||||
<input type="date" class="form-control" ng-model='$ctrl.date'/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Description</label>
|
||||
<textarea class="form-control" ng-model="$ctrl.desc" style="resize:vertical;" required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" type="submit">Add New Visit</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<h3>Previous Visits</h3>
|
||||
<table class="table">
|
||||
<tr ng-repeat="v in $ctrl.visits">
|
||||
<td class="col-sm-2">{{v.date}}</td>
|
||||
<td style="white-space: pre-line">{{v.description}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
73
src/main/resources/static/scss/header.scss
Normal file
@ -0,0 +1,73 @@
|
||||
.navbar {
|
||||
border-top: 4px solid #6db33f;
|
||||
background-color: #34302d;
|
||||
margin-bottom: 0px;
|
||||
border-bottom: 0;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.navbar a.navbar-brand {
|
||||
background: url("../images/spring-logo-dataflow.png") -1px -1px no-repeat;
|
||||
margin: 12px 0 6px;
|
||||
width: 229px;
|
||||
height: 46px;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.navbar a.navbar-brand span {
|
||||
display: block;
|
||||
width: 229px;
|
||||
height: 46px;
|
||||
background: url("../images/spring-logo-dataflow.png") -1px -48px no-repeat;
|
||||
opacity: 0;
|
||||
-moz-transition: opacity 0.12s ease-in-out;
|
||||
-webkit-transition: opacity 0.12s ease-in-out;
|
||||
-o-transition: opacity 0.12s ease-in-out;
|
||||
}
|
||||
|
||||
.navbar a:hover.navbar-brand span {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.navbar li > a, .navbar-text {
|
||||
font-family: "montserratregular", sans-serif;
|
||||
text-shadow: none;
|
||||
font-size: 14px;
|
||||
|
||||
/* line-height: 14px; */
|
||||
padding: 28px 20px;
|
||||
transition: all 0.15s;
|
||||
-webkit-transition: all 0.15s;
|
||||
-moz-transition: all 0.15s;
|
||||
-o-transition: all 0.15s;
|
||||
-ms-transition: all 0.15s;
|
||||
}
|
||||
|
||||
.navbar li > a {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.navbar .navbar-text {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.navbar li:hover > a {
|
||||
color: #eeeeee;
|
||||
background-color: #6db33f;
|
||||
}
|
||||
|
||||
.navbar-toggle {
|
||||
border-width: 0;
|
||||
|
||||
.icon-bar + .icon-bar {
|
||||
margin-top: 3px;
|
||||
}
|
||||
.icon-bar {
|
||||
width: 19px;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
}
|
||||
339
src/main/resources/static/scss/petclinic.scss
Normal file
@ -0,0 +1,339 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
@import "bootstrap";
|
||||
|
||||
$icon-font-path: "../../webjars/bootstrap/fonts/";
|
||||
|
||||
$spring-green: #6db33f;
|
||||
$spring-dark-green: #5fa134;
|
||||
$spring-brown: #34302D;
|
||||
$spring-grey: #838789;
|
||||
$spring-light-grey: #f1f1f1;
|
||||
|
||||
$chatbox-bg-color: #f1f1f1;
|
||||
$chatbox-header-bg-color: #075E54;
|
||||
$chatbox-header-text-color: white;
|
||||
$chatbox-height: 400px;
|
||||
$chatbox-border-radius: 10px;
|
||||
$chatbox-box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
|
||||
$chatbox-bubble-user-bg-color: #dcf8c6;
|
||||
$chatbox-bubble-bot-bg-color: #ffffff;
|
||||
$chatbox-bubble-border-color: #e1e1e1;
|
||||
$chatbox-footer-bg-color: #f9f9f9;
|
||||
$chatbox-input-border-color: #ccc;
|
||||
$chatbox-button-bg-color: #075E54;
|
||||
$chatbox-button-hover-bg-color: #128C7E;
|
||||
|
||||
$body-bg: $spring-light-grey;
|
||||
$text-color: $spring-brown;
|
||||
$link-color: $spring-dark-green;
|
||||
$link-hover-color: $spring-dark-green;
|
||||
|
||||
$navbar-default-link-color: $spring-light-grey;
|
||||
$navbar-default-link-active-color: $spring-light-grey;
|
||||
$navbar-default-link-hover-color: $spring-light-grey;
|
||||
$navbar-default-link-hover-bg: $spring-green;
|
||||
$navbar-default-toggle-icon-bar-bg: $spring-light-grey;
|
||||
$navbar-default-toggle-hover-bg: transparent;
|
||||
$navbar-default-link-active-bg: $spring-green;
|
||||
|
||||
$border-radius-base: 0;
|
||||
$border-radius-large: 0;
|
||||
$border-radius-small: 0;
|
||||
|
||||
$nav-tabs-active-link-hover-color: $spring-light-grey;
|
||||
$nav-tabs-active-link-hover-bg: $spring-brown;
|
||||
$nav-tabs-active-link-hover-border-color: $spring-brown;
|
||||
$nav-tabs-border-color: $spring-brown;
|
||||
|
||||
$pagination-active-bg: $spring-brown;
|
||||
$pagination-active-border: $spring-green;
|
||||
$table-border-color: $spring-brown;
|
||||
|
||||
.table > thead > tr > th {
|
||||
background-color: lighten($spring-brown, 3%);
|
||||
color: $spring-light-grey;
|
||||
}
|
||||
|
||||
.table-filter {
|
||||
background-color: $spring-brown;
|
||||
padding: 9px 12px;
|
||||
}
|
||||
|
||||
.nav > li > a {
|
||||
color: $spring-grey;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
margin-top: 12px;
|
||||
border-width: 2px;
|
||||
transition: border 0.15s;
|
||||
color: $spring-light-grey;
|
||||
background: $spring-brown;
|
||||
border-color: $spring-green;
|
||||
-webkit-transition: border 0.15s;
|
||||
-moz-transition: border 0.15s;
|
||||
-o-transition: border 0.15s;
|
||||
-ms-transition: border 0.15s;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active,
|
||||
&.active,
|
||||
.open .dropdown-toggle {
|
||||
background-color: $spring-brown;
|
||||
border-color: $spring-brown;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.container .text-muted {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.xd-container {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 100px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 15px
|
||||
}
|
||||
|
||||
.index-page--subtitle {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
margin: 0 0 30px;
|
||||
}
|
||||
|
||||
.form-horizontal button.btn-inverse {
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
#job-params-modal .modal-dialog {
|
||||
width: 90%;
|
||||
margin-left:auto;
|
||||
margin-right:auto;
|
||||
}
|
||||
|
||||
[ng-cloak].splash {
|
||||
display: block !important;
|
||||
}
|
||||
[ng-cloak] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.splash {
|
||||
background: $spring-green;
|
||||
color: $spring-brown;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error-page {
|
||||
margin-top: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-page .error-title {
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
margin: 30px 0 0;
|
||||
}
|
||||
|
||||
table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table td .progress {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
table td.action-column {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.help-block {
|
||||
color: lighten($text-color, 50%); // lighten the text some for contrast
|
||||
}
|
||||
|
||||
.xd-containers {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.cluster-view > table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.cluster-view .label, .cluster-view .column-block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cluster-view .input-group-addon {
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
.cluster-view {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.container-details-table th {
|
||||
background-color: lighten($spring-brown, 3%);
|
||||
color: $spring-light-grey;
|
||||
}
|
||||
|
||||
.status-help-content-table td {
|
||||
color: $spring-brown;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.myspinner {
|
||||
animation-name: spinner;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
|
||||
-webkit-transform-origin: 49% 50%;
|
||||
-webkit-animation-name: spinner;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-webkit-animation-timing-function: linear;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-top: 1px dotted $spring-brown;
|
||||
}
|
||||
|
||||
/* Chatbox container */
|
||||
.chatbox {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
width: 300px;
|
||||
background-color: $chatbox-bg-color;
|
||||
border-radius: $chatbox-border-radius;
|
||||
box-shadow: $chatbox-box-shadow;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.minimized {
|
||||
.chatbox-content {
|
||||
height: 40px; /* Height when minimized (header only) */
|
||||
}
|
||||
.chatbox-messages,
|
||||
.chatbox-footer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Header styling */
|
||||
.chatbox-header {
|
||||
background-color: $chatbox-header-bg-color;
|
||||
color: $chatbox-header-text-color;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
border-top-left-radius: $chatbox-border-radius;
|
||||
border-top-right-radius: $chatbox-border-radius;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Chatbox content styling */
|
||||
.chatbox-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: $chatbox-height; /* Adjust to desired height */
|
||||
overflow: hidden; /* Hide overflow to make it scrollable */
|
||||
}
|
||||
|
||||
.chatbox-messages {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto; /* Allows vertical scrolling */
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Chat bubbles styling */
|
||||
.chat-bubble {
|
||||
max-width: 80%;
|
||||
padding: 10px;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
word-wrap: break-word;
|
||||
font-size: 14px;
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&.user {
|
||||
background-color: $chatbox-bubble-user-bg-color; /* WhatsApp-style light green */
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
&.bot {
|
||||
background-color: $chatbox-bubble-bot-bg-color;
|
||||
margin-right: auto;
|
||||
text-align: left;
|
||||
border-bottom-left-radius: 0;
|
||||
border: 1px solid $chatbox-bubble-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
/* Input field and button */
|
||||
.chatbox-footer {
|
||||
padding: 10px;
|
||||
background-color: $chatbox-footer-bg-color;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.chatbox-footer input {
|
||||
flex-grow: 1;
|
||||
padding: 10px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid $chatbox-input-border-color;
|
||||
margin-right: 10px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.chatbox-footer button {
|
||||
background-color: $chatbox-button-bg-color;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: $chatbox-button-hover-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
@import "typography.scss";
|
||||
@import "header.scss";
|
||||
@import "responsive.scss";
|
||||
41
src/main/resources/static/scss/responsive.scss
Normal file
@ -0,0 +1,41 @@
|
||||
@media (max-width: 768px) {
|
||||
.navbar-toggle {
|
||||
position:absolute;
|
||||
z-index: 9999;
|
||||
left:0px;
|
||||
top:0px;
|
||||
}
|
||||
|
||||
.navbar a.navbar-brand {
|
||||
display: block;
|
||||
margin: 0 auto 0 auto;
|
||||
width: 148px;
|
||||
height: 50px;
|
||||
float: none;
|
||||
background: url("../images/spring-logo-dataflow-mobile.png") 0 center no-repeat;
|
||||
}
|
||||
|
||||
.homepage-billboard .homepage-subtitle {
|
||||
font-size: 21px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
.navbar a.navbar-brand span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
.xd-container {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.index-page--subtitle {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
}
|
||||
60
src/main/resources/static/scss/typography.scss
Normal file
@ -0,0 +1,60 @@
|
||||
@font-face {
|
||||
font-family: 'varela_roundregular';
|
||||
|
||||
src: url('../fonts/varela_round-webfont.eot');
|
||||
src: url('../fonts/varela_round-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('../fonts/varela_round-webfont.woff') format('woff'),
|
||||
url('../fonts/varela_round-webfont.ttf') format('truetype'),
|
||||
url('../fonts/varela_round-webfont.svg#varela_roundregular') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'montserratregular';
|
||||
src: url('../fonts/montserrat-webfont.eot');
|
||||
src: url('../fonts/montserrat-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('../fonts/montserrat-webfont.woff') format('woff'),
|
||||
url('../fonts/montserrat-webfont.ttf') format('truetype'),
|
||||
url('../fonts/montserrat-webfont.svg#montserratregular') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
body, h1, h2, h3, p, input {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
font-family: "varela_roundregular", sans-serif;
|
||||
color: #34302d;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
line-height: 30px;
|
||||
font-family: "montserratregular", sans-serif;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
margin-bottom: 10px;
|
||||
font-family: "montserratregular", sans-serif;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
p {
|
||||
//font-size: 15px;
|
||||
//line-height: 24px;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
font-family: "montserratregular", sans-serif;
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package org.springframework.samples.petclinic.api;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
@ActiveProfiles("test")
|
||||
@SpringBootTest
|
||||
class ApiGatewayApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package org.springframework.samples.petclinic.api.application;
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.samples.petclinic.api.dto.Visits;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
class VisitsServiceClientIntegrationTest {
|
||||
|
||||
private static final Integer PET_ID = 1;
|
||||
|
||||
private VisitsServiceClient visitsServiceClient;
|
||||
|
||||
private MockWebServer server;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
server = new MockWebServer();
|
||||
visitsServiceClient = new VisitsServiceClient(WebClient.builder());
|
||||
visitsServiceClient.setHostname(server.url("/").toString());
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void shutdown() throws IOException {
|
||||
this.server.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getVisitsForPets_withAvailableVisitsService() {
|
||||
prepareResponse(response -> response
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody("{\"items\":[{\"id\":5,\"date\":\"2018-11-15\",\"description\":\"test visit\",\"petId\":1}]}"));
|
||||
|
||||
Mono<Visits> visits = visitsServiceClient.getVisitsForPets(Collections.singletonList(1));
|
||||
|
||||
assertVisitDescriptionEquals(visits.block(), PET_ID,"test visit");
|
||||
}
|
||||
|
||||
|
||||
private void assertVisitDescriptionEquals(Visits visits, int petId, String description) {
|
||||
assertEquals(1, visits.items().size());
|
||||
assertNotNull(visits.items().get(0));
|
||||
assertEquals(petId, visits.items().get(0).petId());
|
||||
assertEquals(description, visits.items().get(0).description());
|
||||
}
|
||||
|
||||
private void prepareResponse(Consumer<MockResponse> consumer) {
|
||||
MockResponse response = new MockResponse();
|
||||
consumer.accept(response);
|
||||
this.server.enqueue(response);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
package org.springframework.samples.petclinic.api.boundary.web;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JAutoConfiguration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.samples.petclinic.api.application.CustomersServiceClient;
|
||||
import org.springframework.samples.petclinic.api.application.VisitsServiceClient;
|
||||
import org.springframework.samples.petclinic.api.dto.*;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.net.ConnectException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebFluxTest(controllers = ApiGatewayController.class)
|
||||
@Import({ReactiveResilience4JAutoConfiguration.class, CircuitBreakerConfiguration.class})
|
||||
class ApiGatewayControllerTest {
|
||||
|
||||
@MockBean
|
||||
private CustomersServiceClient customersServiceClient;
|
||||
|
||||
@MockBean
|
||||
private VisitsServiceClient visitsServiceClient;
|
||||
|
||||
@Autowired
|
||||
private WebTestClient client;
|
||||
|
||||
|
||||
@Test
|
||||
void getOwnerDetails_withAvailableVisitsService() {
|
||||
PetDetails cat = PetDetails.PetDetailsBuilder.aPetDetails()
|
||||
.id(20)
|
||||
.name("Garfield")
|
||||
.visits(new ArrayList<>())
|
||||
.build();
|
||||
OwnerDetails owner = OwnerDetails.OwnerDetailsBuilder.anOwnerDetails()
|
||||
.pets(List.of(cat))
|
||||
.build();
|
||||
Mockito
|
||||
.when(customersServiceClient.getOwner(1))
|
||||
.thenReturn(Mono.just(owner));
|
||||
|
||||
VisitDetails visit = new VisitDetails(300, cat.id(), null, "First visit");
|
||||
Visits visits = new Visits(List.of(visit));
|
||||
Mockito
|
||||
.when(visitsServiceClient.getVisitsForPets(Collections.singletonList(cat.id())))
|
||||
.thenReturn(Mono.just(visits));
|
||||
|
||||
client.get()
|
||||
.uri("/api/gateway/owners/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.jsonPath("$.pets[0].name").isEqualTo("Garfield")
|
||||
.jsonPath("$.pets[0].visits[0].description").isEqualTo("First visit");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Resilience4j fallback method
|
||||
*/
|
||||
@Test
|
||||
void getOwnerDetails_withServiceError() {
|
||||
PetDetails cat = PetDetails.PetDetailsBuilder.aPetDetails()
|
||||
.id(20)
|
||||
.name("Garfield")
|
||||
.visits(new ArrayList<>())
|
||||
.build();
|
||||
OwnerDetails owner = OwnerDetails.OwnerDetailsBuilder.anOwnerDetails()
|
||||
.pets(List.of(cat))
|
||||
.build();
|
||||
Mockito
|
||||
.when(customersServiceClient.getOwner(1))
|
||||
.thenReturn(Mono.just(owner));
|
||||
|
||||
Mockito
|
||||
.when(visitsServiceClient.getVisitsForPets(Collections.singletonList(cat.id())))
|
||||
.thenReturn(Mono.error(new ConnectException("Simulate error")));
|
||||
|
||||
client.get()
|
||||
.uri("/api/gateway/owners/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.jsonPath("$.pets[0].name").isEqualTo("Garfield")
|
||||
.jsonPath("$.pets[0].visits").isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package org.springframework.samples.petclinic.api.boundary.web;
|
||||
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
|
||||
import io.github.resilience4j.timelimiter.TimeLimiterRegistry;
|
||||
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
||||
@Configuration
|
||||
public class CircuitBreakerConfiguration {
|
||||
|
||||
@Bean
|
||||
public CircuitBreakerRegistry circuitBreakerRegistry() {
|
||||
return CircuitBreakerRegistry.ofDefaults();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TimeLimiterRegistry timeLimiterRegistry() {
|
||||
return TimeLimiterRegistry.ofDefaults();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public Resilience4JConfigurationProperties resilience4JConfigurationProperties(){
|
||||
return new Resilience4JConfigurationProperties();
|
||||
}
|
||||
|
||||
}
|
||||
568
src/test/jmeter/petclinic_test_plan.jmx
Normal file
@ -0,0 +1,568 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.0 r1840935">
|
||||
<hashTree>
|
||||
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Spring Petclinic Microservices" enabled="true">
|
||||
<stringProp name="TestPlan.comments"></stringProp>
|
||||
<boolProp name="TestPlan.functional_mode">false</boolProp>
|
||||
<boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
|
||||
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
|
||||
<elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="PETCLINC_HOST" elementType="Argument">
|
||||
<stringProp name="Argument.name">PETCLINC_HOST</stringProp>
|
||||
<stringProp name="Argument.value">localhost</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="PETCLINIC_PORT" elementType="Argument">
|
||||
<stringProp name="Argument.name">PETCLINIC_PORT</stringProp>
|
||||
<stringProp name="Argument.value">8080</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="TestPlan.user_define_classpath"></stringProp>
|
||||
</TestPlan>
|
||||
<hashTree>
|
||||
<ConfigTestElement guiclass="HttpDefaultsGui" testclass="ConfigTestElement" testname="HTTP Request Defaults" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="Variables pr<70>-d<>finies" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${PETCLINC_HOST}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${PETCLINIC_PORT}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol"></stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
|
||||
<stringProp name="HTTPSampler.path"></stringProp>
|
||||
<stringProp name="HTTPSampler.concurrentPool">6</stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</ConfigTestElement>
|
||||
<hashTree/>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="" elementType="Header">
|
||||
<stringProp name="Header.name">Content-Type</stringProp>
|
||||
<stringProp name="Header.value">application/json;charset=UTF-8</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate, br</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<kg.apc.jmeter.threads.UltimateThreadGroup guiclass="kg.apc.jmeter.threads.UltimateThreadGroupGui" testclass="kg.apc.jmeter.threads.UltimateThreadGroup" testname="jp@gc - Ultimate Thread Group" enabled="true">
|
||||
<collectionProp name="ultimatethreadgroupdata">
|
||||
<collectionProp name="-111815413">
|
||||
<stringProp name="49">1</stringProp>
|
||||
<stringProp name="0">0</stringProp>
|
||||
<stringProp name="48">0</stringProp>
|
||||
<stringProp name="50547">300</stringProp>
|
||||
<stringProp name="1629">30</stringProp>
|
||||
</collectionProp>
|
||||
</collectionProp>
|
||||
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
|
||||
<boolProp name="LoopController.continue_forever">false</boolProp>
|
||||
<intProp name="LoopController.loops">-1</intProp>
|
||||
</elementProp>
|
||||
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
|
||||
</kg.apc.jmeter.threads.UltimateThreadGroup>
|
||||
<hashTree>
|
||||
<RandomVariableConfig guiclass="TestBeanGUI" testclass="RandomVariableConfig" testname="Pet Type" enabled="true">
|
||||
<stringProp name="variableName">PET_TYPE</stringProp>
|
||||
<stringProp name="outputFormat"></stringProp>
|
||||
<stringProp name="minimumValue">1</stringProp>
|
||||
<stringProp name="maximumValue">6</stringProp>
|
||||
<stringProp name="randomSeed"></stringProp>
|
||||
<boolProp name="perThread">false</boolProp>
|
||||
</RandomVariableConfig>
|
||||
<hashTree/>
|
||||
<GaussianRandomTimer guiclass="GaussianRandomTimerGui" testclass="GaussianRandomTimer" testname="Gaussian Random Timer" enabled="true">
|
||||
<stringProp name="ConstantTimer.delay">300</stringProp>
|
||||
<stringProp name="RandomTimer.range">100.0</stringProp>
|
||||
</GaussianRandomTimer>
|
||||
<hashTree/>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="All Owners" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="Variables pr<70>-d<>finies" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain"></stringProp>
|
||||
<stringProp name="HTTPSampler.port"></stringProp>
|
||||
<stringProp name="HTTPSampler.protocol"></stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/customer/owners</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assertion HTTP 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add Owner" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{ 
|
||||
"firstName":"Firstname",
|
||||
"lastName":"Lastname",
|
||||
"address":"Adress",
|
||||
"city":"City",
|
||||
"telephone":"0000000000"
|
||||
}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain"></stringProp>
|
||||
<stringProp name="HTTPSampler.port"></stringProp>
|
||||
<stringProp name="HTTPSampler.protocol"></stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/customer/owners</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assertion HTTP 201" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49587">201</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
<JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="Owner ID Extractor" enabled="true">
|
||||
<stringProp name="JSONPostProcessor.referenceNames">OWNER_ID</stringProp>
|
||||
<stringProp name="JSONPostProcessor.jsonPathExprs">$.id</stringProp>
|
||||
<stringProp name="JSONPostProcessor.match_numbers"></stringProp>
|
||||
</JSONPostProcessor>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Owner Details" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="Variables pr<70>-d<>finies" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain"></stringProp>
|
||||
<stringProp name="HTTPSampler.port"></stringProp>
|
||||
<stringProp name="HTTPSampler.protocol"></stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/gateway/owners/${OWNER_ID}</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assertion HTTP 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Update Owner" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{ 
|
||||
"id":"${OWNER_ID}",
|
||||
"firstName":"Firstname",
|
||||
"lastName":"Lastname${OWNER_ID}",
|
||||
"address":"Adress${OWNER_ID}",
|
||||
"city":"City${OWNER_ID}",
|
||||
"telephone":"1111111111"
|
||||
}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain"></stringProp>
|
||||
<stringProp name="HTTPSampler.port"></stringProp>
|
||||
<stringProp name="HTTPSampler.protocol"></stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/customer/owners/${OWNER_ID}</stringProp>
|
||||
<stringProp name="HTTPSampler.method">PUT</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assertion HTTP 204" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49590">204</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add Pet" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{ 
|
||||
"name":"Pet",
|
||||
"birthDate":"2018-12-31T23:00:00.000Z",
|
||||
"typeId":"${PET_TYPE}"
|
||||
}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain"></stringProp>
|
||||
<stringProp name="HTTPSampler.port"></stringProp>
|
||||
<stringProp name="HTTPSampler.protocol"></stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/customer/owners/${OWNER_ID}/pets</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assertion HTTP 201" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49587">201</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
<JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="Pet ID Extractor" enabled="true">
|
||||
<stringProp name="JSONPostProcessor.referenceNames">PET_ID</stringProp>
|
||||
<stringProp name="JSONPostProcessor.jsonPathExprs">$.id</stringProp>
|
||||
<stringProp name="JSONPostProcessor.match_numbers"></stringProp>
|
||||
</JSONPostProcessor>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<RandomController guiclass="RandomControlGui" testclass="RandomController" testname="Add Random second Pet" enabled="true">
|
||||
<intProp name="InterleaveControl.style">1</intProp>
|
||||
</RandomController>
|
||||
<hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add Pet" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{ 
|
||||
"name":"Pet",
|
||||
"birthDate":"2018-12-31T23:00:00.000Z",
|
||||
"typeId":"${PET_TYPE}"
|
||||
}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain"></stringProp>
|
||||
<stringProp name="HTTPSampler.port"></stringProp>
|
||||
<stringProp name="HTTPSampler.protocol"></stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/customer/owners/${OWNER_ID}/pets</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assertion HTTP 201" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49587">201</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Update Pet" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{ 
|
||||
"id": ${PET_ID},
|
||||
"name":"Pet${OWNER_ID}",
|
||||
"birthDate":"2018-12-31T23:00:00.000Z",
|
||||
"typeId":"${PET_TYPE}"
|
||||
}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain"></stringProp>
|
||||
<stringProp name="HTTPSampler.port"></stringProp>
|
||||
<stringProp name="HTTPSampler.protocol"></stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/customer/owners/${OWNER_ID}/pets/${PET_ID}</stringProp>
|
||||
<stringProp name="HTTPSampler.method">PUT</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assertion HTTP 204" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49590">204</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add Visit" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{ 
|
||||
"date":"2019-03-15",
|
||||
"description":"Visit"
|
||||
}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain"></stringProp>
|
||||
<stringProp name="HTTPSampler.port"></stringProp>
|
||||
<stringProp name="HTTPSampler.protocol"></stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/visit/owners/${OWNER_ID}/pets/${PET_ID}/visits</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assertion HTTP 201" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49587">201</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<RandomController guiclass="RandomControlGui" testclass="RandomController" testname="Add Random second visit" enabled="true">
|
||||
<intProp name="InterleaveControl.style">1</intProp>
|
||||
</RandomController>
|
||||
<hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add Visit" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{ 
|
||||
"date":"2019-03-15",
|
||||
"description":"Visit"
|
||||
}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain"></stringProp>
|
||||
<stringProp name="HTTPSampler.port"></stringProp>
|
||||
<stringProp name="HTTPSampler.protocol"></stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/visit/owners/${OWNER_ID}/pets/${PET_ID}/visits</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assertion HTTP 201" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49587">201</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="All Vets" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="Variables pr<70>-d<>finies" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain"></stringProp>
|
||||
<stringProp name="HTTPSampler.port"></stringProp>
|
||||
<stringProp name="HTTPSampler.protocol"></stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/vet/vets</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assertion HTTP 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="true">
|
||||
<boolProp name="ResultCollector.error_logging">false</boolProp>
|
||||
<objProp>
|
||||
<name>saveConfig</name>
|
||||
<value class="SampleSaveConfiguration">
|
||||
<time>true</time>
|
||||
<latency>true</latency>
|
||||
<timestamp>true</timestamp>
|
||||
<success>true</success>
|
||||
<label>true</label>
|
||||
<code>true</code>
|
||||
<message>true</message>
|
||||
<threadName>true</threadName>
|
||||
<dataType>true</dataType>
|
||||
<encoding>false</encoding>
|
||||
<assertions>true</assertions>
|
||||
<subresults>true</subresults>
|
||||
<responseData>false</responseData>
|
||||
<samplerData>false</samplerData>
|
||||
<xml>false</xml>
|
||||
<fieldNames>true</fieldNames>
|
||||
<responseHeaders>false</responseHeaders>
|
||||
<requestHeaders>false</requestHeaders>
|
||||
<responseDataOnError>false</responseDataOnError>
|
||||
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
|
||||
<assertionsResultsToSave>0</assertionsResultsToSave>
|
||||
<bytes>true</bytes>
|
||||
<sentBytes>true</sentBytes>
|
||||
<url>true</url>
|
||||
<threadCounts>true</threadCounts>
|
||||
<idleTime>true</idleTime>
|
||||
<connectTime>true</connectTime>
|
||||
</value>
|
||||
</objProp>
|
||||
<stringProp name="filename"></stringProp>
|
||||
</ResultCollector>
|
||||
<hashTree/>
|
||||
<ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="Summary Report" enabled="true">
|
||||
<boolProp name="ResultCollector.error_logging">false</boolProp>
|
||||
<objProp>
|
||||
<name>saveConfig</name>
|
||||
<value class="SampleSaveConfiguration">
|
||||
<time>true</time>
|
||||
<latency>true</latency>
|
||||
<timestamp>true</timestamp>
|
||||
<success>true</success>
|
||||
<label>true</label>
|
||||
<code>true</code>
|
||||
<message>true</message>
|
||||
<threadName>true</threadName>
|
||||
<dataType>true</dataType>
|
||||
<encoding>false</encoding>
|
||||
<assertions>true</assertions>
|
||||
<subresults>true</subresults>
|
||||
<responseData>false</responseData>
|
||||
<samplerData>false</samplerData>
|
||||
<xml>false</xml>
|
||||
<fieldNames>true</fieldNames>
|
||||
<responseHeaders>false</responseHeaders>
|
||||
<requestHeaders>false</requestHeaders>
|
||||
<responseDataOnError>false</responseDataOnError>
|
||||
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
|
||||
<assertionsResultsToSave>0</assertionsResultsToSave>
|
||||
<bytes>true</bytes>
|
||||
<sentBytes>true</sentBytes>
|
||||
<url>true</url>
|
||||
<threadCounts>true</threadCounts>
|
||||
<idleTime>true</idleTime>
|
||||
<connectTime>true</connectTime>
|
||||
</value>
|
||||
</objProp>
|
||||
<stringProp name="filename"></stringProp>
|
||||
</ResultCollector>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</jmeterTestPlan>
|
||||
2
src/test/resources/application-test.yml
Normal file
@ -0,0 +1,2 @@
|
||||
spring.cloud.config.enabled: false
|
||||
eureka.client.enabled: false
|
||||