Initial commit

This commit is contained in:
Florian 2025-10-28 09:06:40 +01:00
commit 6d2fca9a77
76 changed files with 21798 additions and 0 deletions

222
pom.xml Normal file
View 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>

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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()));
}
}

View File

@ -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.");
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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) {
}

View File

@ -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) {
}

View File

@ -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<>());
}
}

View 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

View 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>

View 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

View 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

View File

@ -0,0 +1 @@
# This file is intentionally empty. Message look-ups will fall back to the default "messages.properties" file.

View 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 */

File diff suppressed because it is too large Load Diff

View 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 */

View 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 */

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View 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>

View 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"
});
});

View 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>

View 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>

View 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>

View 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;
}
}

View File

@ -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;
}
}
});

View File

@ -0,0 +1,3 @@
'use strict';
angular.module('infrastructure', []);

View File

@ -0,0 +1,7 @@
'use strict';
angular.module('ownerDetails')
.component('ownerDetails', {
templateUrl: 'scripts/owner-details/owner-details.template.html',
controller: 'OwnerDetailsController'
});

View File

@ -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;
});
}]);

View File

@ -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>'
})
}]);

View File

@ -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>

View File

@ -0,0 +1,7 @@
'use strict';
angular.module('ownerForm')
.component('ownerForm', {
templateUrl: 'scripts/owner-form/owner-form.template.html',
controller: 'OwnerFormController'
});

View File

@ -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');
});
}
};
}]);

View 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>'
})
}]);

View File

@ -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>

View File

@ -0,0 +1,7 @@
'use strict';
angular.module('ownerList')
.component('ownerList', {
templateUrl: 'scripts/owner-list/owner-list.template.html',
controller: 'OwnerListController'
});

View File

@ -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;
});
}]);

View 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>'
})
}]);

View File

@ -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>

View File

@ -0,0 +1,7 @@
'use strict';
angular.module('petForm')
.component('petForm', {
templateUrl: 'scripts/pet-form/pet-form.template.html',
controller: 'PetFormController'
});

View File

@ -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});
});
};
}]);

View 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>'
})
}]);

View File

@ -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>

View File

@ -0,0 +1,7 @@
'use strict';
angular.module('vetList')
.component('vetList', {
templateUrl: 'scripts/vet-list/vet-list.template.html',
controller: 'VetListController'
});

View File

@ -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;
});
}]);

View 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>'
})
}]);

View File

@ -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>

View File

@ -0,0 +1,7 @@
'use strict';
angular.module('visits')
.component('visits', {
templateUrl: 'scripts/visits/visits.template.html',
controller: 'VisitsController'
});

View File

@ -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 });
});
};
}]);

View 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>'
})
}]);

View File

@ -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>

View 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;
}
}

View 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";

View 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;
}
}

View 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;
}

View File

@ -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() {
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View 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">{ &#xd;
&quot;firstName&quot;:&quot;Firstname&quot;,&#xd;
&quot;lastName&quot;:&quot;Lastname&quot;,&#xd;
&quot;address&quot;:&quot;Adress&quot;,&#xd;
&quot;city&quot;:&quot;City&quot;,&#xd;
&quot;telephone&quot;:&quot;0000000000&quot;&#xd;
}</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">{ &#xd;
&quot;id&quot;:&quot;${OWNER_ID}&quot;,&#xd;
&quot;firstName&quot;:&quot;Firstname&quot;,&#xd;
&quot;lastName&quot;:&quot;Lastname${OWNER_ID}&quot;,&#xd;
&quot;address&quot;:&quot;Adress${OWNER_ID}&quot;,&#xd;
&quot;city&quot;:&quot;City${OWNER_ID}&quot;,&#xd;
&quot;telephone&quot;:&quot;1111111111&quot;&#xd;
}</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">{ &#xd;
&quot;name&quot;:&quot;Pet&quot;,&#xd;
&quot;birthDate&quot;:&quot;2018-12-31T23:00:00.000Z&quot;,&#xd;
&quot;typeId&quot;:&quot;${PET_TYPE}&quot;&#xd;
}</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">{ &#xd;
&quot;name&quot;:&quot;Pet&quot;,&#xd;
&quot;birthDate&quot;:&quot;2018-12-31T23:00:00.000Z&quot;,&#xd;
&quot;typeId&quot;:&quot;${PET_TYPE}&quot;&#xd;
}</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">{ &#xd;
&quot;id&quot;: ${PET_ID},&#xd;
&quot;name&quot;:&quot;Pet${OWNER_ID}&quot;,&#xd;
&quot;birthDate&quot;:&quot;2018-12-31T23:00:00.000Z&quot;,&#xd;
&quot;typeId&quot;:&quot;${PET_TYPE}&quot;&#xd;
}</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">{ &#xd;
&quot;date&quot;:&quot;2019-03-15&quot;,&#xd;
&quot;description&quot;:&quot;Visit&quot;&#xd;
}</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">{ &#xd;
&quot;date&quot;:&quot;2019-03-15&quot;,&#xd;
&quot;description&quot;:&quot;Visit&quot;&#xd;
}</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>

View File

@ -0,0 +1,2 @@
spring.cloud.config.enabled: false
eureka.client.enabled: false