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